mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 13:29:27 +01:00
per-user settings for nickname enforcement
This commit is contained in:
parent
d0ded906d4
commit
2ee89b15b3
111
irc/accounts.go
111
irc/accounts.go
@ -30,6 +30,7 @@ const (
|
|||||||
keyAccountRegTime = "account.registered.time %s"
|
keyAccountRegTime = "account.registered.time %s"
|
||||||
keyAccountCredentials = "account.credentials %s"
|
keyAccountCredentials = "account.credentials %s"
|
||||||
keyAccountAdditionalNicks = "account.additionalnicks %s"
|
keyAccountAdditionalNicks = "account.additionalnicks %s"
|
||||||
|
keyAccountEnforcement = "account.customenforcement %s"
|
||||||
keyAccountVHost = "account.vhost %s"
|
keyAccountVHost = "account.vhost %s"
|
||||||
keyCertToAccount = "account.creds.certfp %s"
|
keyCertToAccount = "account.creds.certfp %s"
|
||||||
|
|
||||||
@ -53,12 +54,14 @@ type AccountManager struct {
|
|||||||
// track clients logged in to accounts
|
// track clients logged in to accounts
|
||||||
accountToClients map[string][]*Client
|
accountToClients map[string][]*Client
|
||||||
nickToAccount map[string]string
|
nickToAccount map[string]string
|
||||||
|
accountToMethod map[string]NickReservationMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccountManager(server *Server) *AccountManager {
|
func NewAccountManager(server *Server) *AccountManager {
|
||||||
am := AccountManager{
|
am := AccountManager{
|
||||||
accountToClients: make(map[string][]*Client),
|
accountToClients: make(map[string][]*Client),
|
||||||
nickToAccount: make(map[string]string),
|
nickToAccount: make(map[string]string),
|
||||||
|
accountToMethod: make(map[string]NickReservationMethod),
|
||||||
server: server,
|
server: server,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +75,8 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]string)
|
nickToAccount := make(map[string]string)
|
||||||
|
accountToMethod := make(map[string]NickReservationMethod)
|
||||||
existsPrefix := fmt.Sprintf(keyAccountExists, "")
|
existsPrefix := fmt.Sprintf(keyAccountExists, "")
|
||||||
|
|
||||||
am.serialCacheUpdateMutex.Lock()
|
am.serialCacheUpdateMutex.Lock()
|
||||||
@ -83,14 +87,22 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
|||||||
if !strings.HasPrefix(key, existsPrefix) {
|
if !strings.HasPrefix(key, existsPrefix) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
accountName := strings.TrimPrefix(key, existsPrefix)
|
|
||||||
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil {
|
account := strings.TrimPrefix(key, existsPrefix)
|
||||||
result[accountName] = accountName
|
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)
|
additionalNicks := unmarshalReservedNicks(rawNicks)
|
||||||
for _, nick := range additionalNicks {
|
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
|
return true
|
||||||
@ -102,7 +114,8 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
|||||||
am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error())
|
am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error())
|
||||||
} else {
|
} else {
|
||||||
am.Lock()
|
am.Lock()
|
||||||
am.nickToAccount = result
|
am.nickToAccount = nickToAccount
|
||||||
|
am.accountToMethod = accountToMethod
|
||||||
am.Unlock()
|
am.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,6 +169,84 @@ func (am *AccountManager) NickToAccount(nick string) string {
|
|||||||
return am.nickToAccount[cfnick]
|
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) {
|
func (am *AccountManager) AccountToClients(account string) (result []*Client) {
|
||||||
cfaccount, err := CasefoldName(account)
|
cfaccount, err := CasefoldName(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -992,10 +1083,12 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo)
|
|||||||
|
|
||||||
func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
||||||
changed := client.SetAccountName(account.Name)
|
changed := client.SetAccountName(account.Name)
|
||||||
if changed {
|
if !changed {
|
||||||
go client.nickTimer.Touch()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.nickTimer.Touch()
|
||||||
|
|
||||||
am.applyVHostInfo(client, account.VHost)
|
am.applyVHostInfo(client, account.VHost)
|
||||||
|
|
||||||
casefoldedAccount := client.Account()
|
casefoldedAccount := client.Account()
|
||||||
|
@ -119,12 +119,7 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var reservedAccount string
|
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick)
|
||||||
var method NickReservationMethod
|
|
||||||
if client.server.AccountConfig().NickReservation.Enabled {
|
|
||||||
reservedAccount = client.server.accounts.NickToAccount(newcfnick)
|
|
||||||
method = client.server.AccountConfig().NickReservation.Method
|
|
||||||
}
|
|
||||||
|
|
||||||
clients.Lock()
|
clients.Lock()
|
||||||
defer clients.Unlock()
|
defer clients.Unlock()
|
||||||
|
@ -8,7 +8,6 @@ package irc
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
@ -105,10 +104,50 @@ type VHostConfig struct {
|
|||||||
type NickReservationMethod int
|
type NickReservationMethod int
|
||||||
|
|
||||||
const (
|
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
|
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 {
|
func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
var orig, raw string
|
var orig, raw string
|
||||||
var err error
|
var err error
|
||||||
@ -118,20 +157,18 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
|
|||||||
if raw, err = Casefold(orig); err != nil {
|
if raw, err = Casefold(orig); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if raw == "timeout" {
|
method, err := nickReservationFromString(raw)
|
||||||
*nr = NickReservationWithTimeout
|
if err == nil {
|
||||||
} else if raw == "strict" {
|
*nr = method
|
||||||
*nr = NickReservationStrict
|
|
||||||
} else {
|
|
||||||
return errors.New(fmt.Sprintf("invalid nick-reservation.method value: %s", orig))
|
|
||||||
}
|
}
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type NickReservationConfig struct {
|
type NickReservationConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
AdditionalNickLimit int `yaml:"additional-nick-limit"`
|
AdditionalNickLimit int `yaml:"additional-nick-limit"`
|
||||||
Method NickReservationMethod
|
Method NickReservationMethod
|
||||||
|
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
|
||||||
RenameTimeout time.Duration `yaml:"rename-timeout"`
|
RenameTimeout time.Duration `yaml:"rename-timeout"`
|
||||||
RenamePrefix string `yaml:"rename-prefix"`
|
RenamePrefix string `yaml:"rename-prefix"`
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ var (
|
|||||||
errSaslFail = errors.New("SASL failed")
|
errSaslFail = errors.New("SASL failed")
|
||||||
errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token")
|
errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token")
|
||||||
errInvalidUsername = errors.New("Invalid username")
|
errInvalidUsername = errors.New("Invalid username")
|
||||||
|
errFeatureDisabled = errors.New("That feature is disabled")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Socket Errors
|
// Socket Errors
|
||||||
|
@ -189,14 +189,14 @@ type NickTimer struct {
|
|||||||
// NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled)
|
// NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled)
|
||||||
func NewNickTimer(client *Client) *NickTimer {
|
func NewNickTimer(client *Client) *NickTimer {
|
||||||
config := client.server.AccountConfig().NickReservation
|
config := client.server.AccountConfig().NickReservation
|
||||||
if !(config.Enabled && config.Method == NickReservationWithTimeout) {
|
if !(config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
nt := NickTimer{
|
|
||||||
|
return &NickTimer{
|
||||||
client: client,
|
client: client,
|
||||||
timeout: config.RenameTimeout,
|
timeout: config.RenameTimeout,
|
||||||
}
|
}
|
||||||
return &nt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Touch records a nick change and updates the timer as necessary
|
// Touch records a nick change and updates the timer as necessary
|
||||||
@ -207,7 +207,8 @@ func (nt *NickTimer) Touch() {
|
|||||||
|
|
||||||
nick := nt.client.NickCasefolded()
|
nick := nt.client.NickCasefolded()
|
||||||
account := nt.client.Account()
|
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
|
var shouldWarn bool
|
||||||
|
|
||||||
@ -227,11 +228,11 @@ func (nt *NickTimer) Touch() {
|
|||||||
nt.accountForNick = accountForNick
|
nt.accountForNick = accountForNick
|
||||||
delinquent := accountForNick != "" && accountForNick != account
|
delinquent := accountForNick != "" && accountForNick != account
|
||||||
|
|
||||||
if nt.timer != nil && (!delinquent || accountChanged) {
|
if nt.timer != nil && (!enforceTimeout || !delinquent || accountChanged) {
|
||||||
nt.timer.Stop()
|
nt.timer.Stop()
|
||||||
nt.timer = nil
|
nt.timer = nil
|
||||||
}
|
}
|
||||||
if delinquent && accountChanged {
|
if enforceTimeout && delinquent && accountChanged {
|
||||||
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
|
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
|
||||||
shouldWarn = true
|
shouldWarn = true
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,11 @@ func nsGroupEnabled(server *Server) bool {
|
|||||||
return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled
|
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.
|
const nickservHelp = `NickServ lets you register and login to an account.
|
||||||
|
|
||||||
To see in-depth help for a specific NickServ command, try:
|
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,
|
enabled: servCmdRequiresAccreg,
|
||||||
authRequired: true,
|
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": {
|
"ghost": {
|
||||||
handler: nsGhostHandler,
|
handler: nsGhostHandler,
|
||||||
help: `Syntax: $bGHOST <nickname>$b
|
help: `Syntax: $bGHOST <nickname>$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"))
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -206,12 +206,19 @@ accounts:
|
|||||||
additional-nick-limit: 2
|
additional-nick-limit: 2
|
||||||
|
|
||||||
# method describes how nickname reservation is handled
|
# method describes how nickname reservation is handled
|
||||||
|
# already logged-in using SASL or NickServ
|
||||||
# timeout: let the user change to the registered nickname, give them X seconds
|
# 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
|
# 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
|
# 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
|
||||||
|
# optional: no enforcement by default, but allow users to opt in to
|
||||||
|
# the enforcement level of their choice
|
||||||
method: timeout
|
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 - this is how long users have 'til they're renamed
|
||||||
rename-timeout: 30s
|
rename-timeout: 30s
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user