From 2ee89b15b398099d4c84d68296f63dd7e051137d Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 2 Jan 2019 10:08:44 -0500 Subject: [PATCH] per-user settings for nickname enforcement --- irc/accounts.go | 111 +++++++++++++++++++++++++++++++++++---- irc/client_lookup_set.go | 7 +-- irc/config.go | 65 ++++++++++++++++++----- irc/errors.go | 1 + irc/idletimer.go | 13 ++--- irc/nickserv.go | 43 +++++++++++++++ oragono.yaml | 15 ++++-- 7 files changed, 216 insertions(+), 39 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index f676cba2..612e0b14 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -30,6 +30,7 @@ const ( keyAccountRegTime = "account.registered.time %s" keyAccountCredentials = "account.credentials %s" keyAccountAdditionalNicks = "account.additionalnicks %s" + keyAccountEnforcement = "account.customenforcement %s" keyAccountVHost = "account.vhost %s" keyCertToAccount = "account.creds.certfp %s" @@ -53,12 +54,14 @@ type AccountManager struct { // track clients logged in to accounts accountToClients map[string][]*Client nickToAccount map[string]string + accountToMethod map[string]NickReservationMethod } func NewAccountManager(server *Server) *AccountManager { am := AccountManager{ accountToClients: make(map[string][]*Client), nickToAccount: make(map[string]string), + accountToMethod: make(map[string]NickReservationMethod), server: server, } @@ -72,7 +75,8 @@ func (am *AccountManager) buildNickToAccountIndex() { return } - result := make(map[string]string) + nickToAccount := make(map[string]string) + accountToMethod := make(map[string]NickReservationMethod) existsPrefix := fmt.Sprintf(keyAccountExists, "") am.serialCacheUpdateMutex.Lock() @@ -83,14 +87,22 @@ func (am *AccountManager) buildNickToAccountIndex() { if !strings.HasPrefix(key, existsPrefix) { return false } - accountName := strings.TrimPrefix(key, existsPrefix) - if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil { - result[accountName] = accountName + + account := strings.TrimPrefix(key, existsPrefix) + if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil { + nickToAccount[account] = account } - if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, accountName)); err == nil { + if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, account)); err == nil { additionalNicks := unmarshalReservedNicks(rawNicks) for _, nick := range additionalNicks { - result[nick] = accountName + nickToAccount[nick] = account + } + } + + if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil { + method, err := nickReservationFromString(methodStr) + if err == nil { + accountToMethod[account] = method } } return true @@ -102,7 +114,8 @@ func (am *AccountManager) buildNickToAccountIndex() { am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error()) } else { am.Lock() - am.nickToAccount = result + am.nickToAccount = nickToAccount + am.accountToMethod = accountToMethod am.Unlock() } } @@ -156,6 +169,84 @@ func (am *AccountManager) NickToAccount(nick string) string { return am.nickToAccount[cfnick] } +// Given a nick, looks up the account that owns it and the method (none/timeout/strict) +// used to enforce ownership. +func (am *AccountManager) EnforcementStatus(nick string) (account string, method NickReservationMethod) { + cfnick, err := CasefoldName(nick) + if err != nil { + return + } + + config := am.server.Config() + if !config.Accounts.NickReservation.Enabled { + method = NickReservationNone + return + } + + am.RLock() + defer am.RUnlock() + + account = am.nickToAccount[cfnick] + method = am.accountToMethod[account] + // if they don't have a custom setting, or customization is disabled, use the default + if method == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement { + method = config.Accounts.NickReservation.Method + } + if method == NickReservationOptional { + // enforcement was explicitly enabled neither in the config or by the user + method = NickReservationNone + } + return +} + +// Looks up the enforcement method stored in the database for an account +// (typically you want EnforcementStatus instead, which respects the config) +func (am *AccountManager) getStoredEnforcementStatus(account string) string { + am.RLock() + defer am.RUnlock() + return nickReservationToString(am.accountToMethod[account]) +} + +// Sets a custom enforcement method for an account and stores it in the database. +func (am *AccountManager) SetEnforcementStatus(account string, method NickReservationMethod) (err error) { + config := am.server.Config() + if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) { + return errFeatureDisabled + } + + var serialized string + if method == NickReservationOptional { + serialized = "" // normally this is "default", but we're going to delete the key + } else { + serialized = nickReservationToString(method) + } + + key := fmt.Sprintf(keyAccountEnforcement, account) + + am.Lock() + defer am.Unlock() + + currentMethod := am.accountToMethod[account] + if method != currentMethod { + if method == NickReservationOptional { + delete(am.accountToMethod, account) + } else { + am.accountToMethod[account] = method + } + + return am.server.store.Update(func(tx *buntdb.Tx) (err error) { + if serialized != "" { + _, _, err = tx.Set(key, nickReservationToString(method), nil) + } else { + _, err = tx.Delete(key) + } + return + }) + } + + return nil +} + func (am *AccountManager) AccountToClients(account string) (result []*Client) { cfaccount, err := CasefoldName(account) if err != nil { @@ -992,10 +1083,12 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo) func (am *AccountManager) Login(client *Client, account ClientAccount) { changed := client.SetAccountName(account.Name) - if changed { - go client.nickTimer.Touch() + if !changed { + return } + client.nickTimer.Touch() + am.applyVHostInfo(client, account.VHost) casefoldedAccount := client.Account() diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 6a3a749c..387a357a 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -119,12 +119,7 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error { return err } - var reservedAccount string - var method NickReservationMethod - if client.server.AccountConfig().NickReservation.Enabled { - reservedAccount = client.server.accounts.NickToAccount(newcfnick) - method = client.server.AccountConfig().NickReservation.Method - } + reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick) clients.Lock() defer clients.Unlock() diff --git a/irc/config.go b/irc/config.go index 8003eb5b..272b91bc 100644 --- a/irc/config.go +++ b/irc/config.go @@ -8,7 +8,6 @@ package irc import ( "crypto/tls" "encoding/json" - "errors" "fmt" "io/ioutil" "log" @@ -105,10 +104,50 @@ type VHostConfig struct { type NickReservationMethod int const ( - NickReservationWithTimeout NickReservationMethod = iota + // NickReservationOptional is the zero value; it serializes to + // "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`. + // in both cases, it means "defer to the other source of truth", i.e., + // in the config, defer to the user's custom setting, and as a custom setting, + // defer to the default in the config. if both are NickReservationOptional then + // there is no enforcement. + NickReservationOptional NickReservationMethod = iota + NickReservationNone + NickReservationWithTimeout NickReservationStrict ) +func nickReservationToString(method NickReservationMethod) string { + switch method { + case NickReservationOptional: + return "default" + case NickReservationNone: + return "none" + case NickReservationWithTimeout: + return "timeout" + case NickReservationStrict: + return "strict" + default: + return "" + } +} + +func nickReservationFromString(method string) (NickReservationMethod, error) { + switch method { + case "default": + return NickReservationOptional, nil + case "optional": + return NickReservationOptional, nil + case "none": + return NickReservationNone, nil + case "timeout": + return NickReservationWithTimeout, nil + case "strict": + return NickReservationStrict, nil + default: + return NickReservationOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method) + } +} + func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error { var orig, raw string var err error @@ -118,22 +157,20 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error if raw, err = Casefold(orig); err != nil { return err } - if raw == "timeout" { - *nr = NickReservationWithTimeout - } else if raw == "strict" { - *nr = NickReservationStrict - } else { - return errors.New(fmt.Sprintf("invalid nick-reservation.method value: %s", orig)) + method, err := nickReservationFromString(raw) + if err == nil { + *nr = method } - return nil + return err } type NickReservationConfig struct { - Enabled bool - AdditionalNickLimit int `yaml:"additional-nick-limit"` - Method NickReservationMethod - RenameTimeout time.Duration `yaml:"rename-timeout"` - RenamePrefix string `yaml:"rename-prefix"` + Enabled bool + AdditionalNickLimit int `yaml:"additional-nick-limit"` + Method NickReservationMethod + AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"` + RenameTimeout time.Duration `yaml:"rename-timeout"` + RenamePrefix string `yaml:"rename-prefix"` } // ChannelRegistrationConfig controls channel registration. diff --git a/irc/errors.go b/irc/errors.go index db969071..a906a391 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -40,6 +40,7 @@ var ( errSaslFail = errors.New("SASL failed") errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token") errInvalidUsername = errors.New("Invalid username") + errFeatureDisabled = errors.New("That feature is disabled") ) // Socket Errors diff --git a/irc/idletimer.go b/irc/idletimer.go index f4850a80..2aa32a58 100644 --- a/irc/idletimer.go +++ b/irc/idletimer.go @@ -189,14 +189,14 @@ type NickTimer struct { // 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) { + if !(config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)) { return nil } - nt := NickTimer{ + + return &NickTimer{ client: client, timeout: config.RenameTimeout, } - return &nt } // Touch records a nick change and updates the timer as necessary @@ -207,7 +207,8 @@ func (nt *NickTimer) Touch() { nick := nt.client.NickCasefolded() account := nt.client.Account() - accountForNick := nt.client.server.accounts.NickToAccount(nick) + accountForNick, method := nt.client.server.accounts.EnforcementStatus(nick) + enforceTimeout := method == NickReservationWithTimeout var shouldWarn bool @@ -227,11 +228,11 @@ func (nt *NickTimer) Touch() { nt.accountForNick = accountForNick delinquent := accountForNick != "" && accountForNick != account - if nt.timer != nil && (!delinquent || accountChanged) { + if nt.timer != nil && (!enforceTimeout || !delinquent || accountChanged) { nt.timer.Stop() nt.timer = nil } - if delinquent && accountChanged { + if enforceTimeout && delinquent && accountChanged { nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout) shouldWarn = true } diff --git a/irc/nickserv.go b/irc/nickserv.go index c5a85617..936f0819 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -25,6 +25,11 @@ func nsGroupEnabled(server *Server) bool { return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled } +func nsEnforceEnabled(server *Server) bool { + config := server.Config() + return config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement +} + const nickservHelp = `NickServ lets you register and login to an account. To see in-depth help for a specific NickServ command, try: @@ -44,6 +49,22 @@ DROP de-links the given (or your current) nickname from your user account.`, enabled: servCmdRequiresAccreg, authRequired: true, }, + "enforce": { + handler: nsEnforceHandler, + help: `Syntax: $bENFORCE [method]$b + +ENFORCE lets you specify a custom enforcement mechanism for your registered +nicknames. Your options are: +1. 'none' [no enforcement, overriding the server default] +2. 'timeout' [anyone using the nick must authenticate before a deadline, + or else they will be renamed] +3. 'strict' [you must already be authenticated to use the nick] +4. 'default' [use the server default] +With no arguments, queries your current enforcement status.`, + helpShort: `$bENFORCE$b lets you change how your nicknames are reserved.`, + authRequired: true, + enabled: nsEnforceEnabled, + }, "ghost": { handler: nsGhostHandler, help: `Syntax: $bGHOST $b @@ -464,3 +485,25 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb nsNotice(rb, client.t("Password could not be changed due to server error")) } } + +func nsEnforceHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { + arg := strings.TrimSpace(params) + + if arg == "" { + status := server.accounts.getStoredEnforcementStatus(client.Account()) + nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status)) + } else { + method, err := nickReservationFromString(arg) + if err != nil { + nsNotice(rb, client.t("Invalid parameters")) + return + } + err = server.accounts.SetEnforcementStatus(client.Account(), method) + if err == nil { + nsNotice(rb, client.t("Enforcement method set")) + } else { + server.logger.Error("internal", "couldn't store NS ENFORCE data", err.Error()) + nsNotice(rb, client.t("An error occurred")) + } + } +} diff --git a/oragono.yaml b/oragono.yaml index 0ff89d78..3d9966ce 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -206,12 +206,19 @@ accounts: additional-nick-limit: 2 # method describes how nickname reservation is handled - # timeout: let the user change to the registered nickname, give them X seconds - # to login and then rename them if they haven't done so - # strict: don't let the user change to the registered nickname unless they're - # already logged-in using SASL or NickServ + # already logged-in using SASL or NickServ + # timeout: let the user change to the registered nickname, give them X seconds + # to login and then rename them if they haven't done so + # strict: don't let the user change to the registered nickname unless they're + # already logged-in using SASL or NickServ + # optional: no enforcement by default, but allow users to opt in to + # the enforcement level of their choice method: timeout + # allow users to set their own nickname enforcement status, e.g., + # to opt in to strict enforcement + allow-custom-enforcement: true + # rename-timeout - this is how long users have 'til they're renamed rename-timeout: 30s