3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-12-23 19:22:41 +01:00
ergo/irc/accounts.go
Alex Jaspersen 1d832ee1bc Show email in NS INFO when user has permission.
Users logged into an account can see their own email.
Opers with the accreg capability can see all users.
2021-06-04 16:44:00 -07:00

2059 lines
58 KiB
Go

// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
"bytes"
"crypto/x509"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode"
"github.com/ergochat/ergo/irc/connection_limits"
"github.com/ergochat/ergo/irc/email"
"github.com/ergochat/ergo/irc/migrations"
"github.com/ergochat/ergo/irc/modes"
"github.com/ergochat/ergo/irc/passwd"
"github.com/ergochat/ergo/irc/utils"
"github.com/tidwall/buntdb"
)
const (
keyAccountExists = "account.exists %s"
keyAccountVerified = "account.verified %s"
keyAccountUnregistered = "account.unregistered %s"
keyAccountCallback = "account.callback %s"
keyAccountVerificationCode = "account.verificationcode %s"
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
keyAccountRegTime = "account.registered.time %s"
keyAccountCredentials = "account.credentials %s"
keyAccountAdditionalNicks = "account.additionalnicks %s"
keyAccountSettings = "account.settings %s"
keyAccountVHost = "account.vhost %s"
keyCertToAccount = "account.creds.certfp %s"
keyAccountChannels = "account.channels %s" // channels registered to the account
keyAccountLastSeen = "account.lastseen %s"
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
keyAccountRealname = "account.realname %s" // client realname stored as string
keyAccountSuspended = "account.suspended %s" // client realname stored as string
// for an always-on client, a map of channel names they're in to their current modes
// (not to be confused with their amodes, which a non-always-on client can have):
keyAccountChannelToModes = "account.channeltomodes %s"
maxCertfpsPerAccount = 5
)
// everything about accounts is persistent; therefore, the database is the authoritative
// source of truth for all account information. anything on the heap is just a cache
type AccountManager struct {
sync.RWMutex // tier 2
serialCacheUpdateMutex sync.Mutex // tier 3
server *Server
// track clients logged in to accounts
accountToClients map[string][]*Client
nickToAccount map[string]string
skeletonToAccount map[string]string
accountToMethod map[string]NickEnforcementMethod
registerThrottle connection_limits.GenericThrottle
}
func (am *AccountManager) Initialize(server *Server) {
am.accountToClients = make(map[string][]*Client)
am.nickToAccount = make(map[string]string)
am.skeletonToAccount = make(map[string]string)
am.accountToMethod = make(map[string]NickEnforcementMethod)
am.server = server
config := server.Config()
am.buildNickToAccountIndex(config)
am.createAlwaysOnClients(config)
am.resetRegisterThrottle(config)
}
func (am *AccountManager) resetRegisterThrottle(config *Config) {
am.Lock()
defer am.Unlock()
am.registerThrottle = connection_limits.GenericThrottle{
Duration: config.Accounts.Registration.Throttling.Duration,
Limit: config.Accounts.Registration.Throttling.MaxAttempts,
}
}
func (am *AccountManager) touchRegisterThrottle() (throttled bool) {
am.Lock()
defer am.Unlock()
throttled, _ = am.registerThrottle.Touch()
return
}
func (am *AccountManager) createAlwaysOnClients(config *Config) {
if config.Accounts.Multiclient.AlwaysOn == PersistentDisabled {
return
}
verifiedPrefix := fmt.Sprintf(keyAccountVerified, "")
am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock()
var accounts []string
am.server.store.View(func(tx *buntdb.Tx) error {
err := tx.AscendGreaterOrEqual("", verifiedPrefix, func(key, value string) bool {
if !strings.HasPrefix(key, verifiedPrefix) {
return false
}
account := strings.TrimPrefix(key, verifiedPrefix)
accounts = append(accounts, account)
return true
})
return err
})
for _, accountName := range accounts {
account, err := am.LoadAccount(accountName)
if err == nil && (account.Verified && account.Suspended == nil) &&
persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn) {
am.server.AddAlwaysOnClient(
account,
am.loadChannels(accountName),
am.loadLastSeen(accountName),
am.loadModes(accountName),
am.loadRealname(accountName),
)
}
}
}
func (am *AccountManager) buildNickToAccountIndex(config *Config) {
if !config.Accounts.NickReservation.Enabled {
return
}
nickToAccount := make(map[string]string)
skeletonToAccount := make(map[string]string)
accountToMethod := make(map[string]NickEnforcementMethod)
existsPrefix := fmt.Sprintf(keyAccountExists, "")
am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock()
err := am.server.store.View(func(tx *buntdb.Tx) error {
err := tx.AscendGreaterOrEqual("", existsPrefix, func(key, value string) bool {
if !strings.HasPrefix(key, existsPrefix) {
return false
}
account := strings.TrimPrefix(key, existsPrefix)
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil {
nickToAccount[account] = account
accountName, err := tx.Get(fmt.Sprintf(keyAccountName, account))
if err != nil {
am.server.logger.Error("internal", "missing account name for", account)
} else {
skeleton, _ := Skeleton(accountName)
skeletonToAccount[skeleton] = account
}
}
if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, account)); err == nil {
additionalNicks := unmarshalReservedNicks(rawNicks)
for _, nick := range additionalNicks {
cfnick, _ := CasefoldName(nick)
nickToAccount[cfnick] = account
skeleton, _ := Skeleton(nick)
skeletonToAccount[skeleton] = account
}
}
if rawPrefs, err := tx.Get(fmt.Sprintf(keyAccountSettings, account)); err == nil {
var prefs AccountSettings
err := json.Unmarshal([]byte(rawPrefs), &prefs)
if err == nil && prefs.NickEnforcement != NickEnforcementOptional {
accountToMethod[account] = prefs.NickEnforcement
} else if err != nil {
am.server.logger.Error("internal", "corrupt account creds", account)
}
}
return true
})
return err
})
if config.Accounts.NickReservation.Method == NickEnforcementStrict {
unregisteredPrefix := fmt.Sprintf(keyAccountUnregistered, "")
am.server.store.View(func(tx *buntdb.Tx) error {
tx.AscendGreaterOrEqual("", unregisteredPrefix, func(key, value string) bool {
if !strings.HasPrefix(key, unregisteredPrefix) {
return false
}
account := strings.TrimPrefix(key, unregisteredPrefix)
accountName := value
nickToAccount[account] = account
skeleton, _ := Skeleton(accountName)
skeletonToAccount[skeleton] = account
return true
})
return nil
})
}
if err != nil {
am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error())
} else {
am.Lock()
am.nickToAccount = nickToAccount
am.skeletonToAccount = skeletonToAccount
am.accountToMethod = accountToMethod
am.Unlock()
}
}
func (am *AccountManager) NickToAccount(nick string) string {
cfnick, err := CasefoldName(nick)
if err != nil {
return ""
}
skel, err := Skeleton(nick)
if err != nil {
return ""
}
am.RLock()
defer am.RUnlock()
account := am.nickToAccount[cfnick]
if account != "" {
return account
}
return am.skeletonToAccount[skel]
}
// given an account, combine stored enforcement method with the config settings
// to compute the actual enforcement method
func configuredEnforcementMethod(config *Config, storedMethod NickEnforcementMethod) (result NickEnforcementMethod) {
if !config.Accounts.NickReservation.Enabled {
return NickEnforcementNone
}
result = storedMethod
// if they don't have a custom setting, or customization is disabled, use the default
if result == NickEnforcementOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
result = config.Accounts.NickReservation.Method
}
if result == NickEnforcementOptional {
// enforcement was explicitly enabled neither in the config or by the user
result = NickEnforcementNone
}
return
}
// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
// used to enforce ownership.
func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickEnforcementMethod) {
config := am.server.Config()
if !config.Accounts.NickReservation.Enabled {
return "", NickEnforcementNone
}
am.RLock()
defer am.RUnlock()
finalEnforcementMethod := func(account_ string) (result NickEnforcementMethod) {
storedMethod := am.accountToMethod[account_]
return configuredEnforcementMethod(config, storedMethod)
}
nickAccount := am.nickToAccount[cfnick]
skelAccount := am.skeletonToAccount[skeleton]
if nickAccount == "" && skelAccount == "" {
return "", NickEnforcementNone
} else if nickAccount != "" && (skelAccount == nickAccount || skelAccount == "") {
return nickAccount, finalEnforcementMethod(nickAccount)
} else if skelAccount != "" && nickAccount == "" {
return skelAccount, finalEnforcementMethod(skelAccount)
} else {
// nickAccount != skelAccount and both are nonempty:
// two people have competing claims on (this casefolding of) this nick!
nickMethod := finalEnforcementMethod(nickAccount)
skelMethod := finalEnforcementMethod(skelAccount)
switch {
case skelMethod == NickEnforcementNone:
return nickAccount, nickMethod
case nickMethod == NickEnforcementNone:
return skelAccount, skelMethod
default:
// nobody can use this nick
return "!", NickEnforcementStrict
}
}
}
// Sets a custom enforcement method for an account and stores it in the database.
func (am *AccountManager) SetEnforcementStatus(account string, method NickEnforcementMethod) (finalSettings AccountSettings, err error) {
config := am.server.Config()
if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
err = errFeatureDisabled
return
}
setter := func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.NickEnforcement = method
return out, nil
}
_, err = am.ModifyAccountSettings(account, setter)
if err != nil {
return
}
// this update of the data plane is racey, but it's probably fine
am.Lock()
defer am.Unlock()
if method == NickEnforcementOptional {
delete(am.accountToMethod, account)
} else {
am.accountToMethod[account] = method
}
return
}
func (am *AccountManager) AccountToClients(account string) (result []*Client) {
cfaccount, err := CasefoldName(account)
if err != nil {
return
}
am.RLock()
defer am.RUnlock()
return am.accountToClients[cfaccount]
}
func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
casefoldedAccount, err := CasefoldName(account)
skeleton, skerr := Skeleton(account)
if err != nil || skerr != nil || account == "" || account == "*" {
return errAccountCreation
}
if restrictedCasefoldedNicks.Has(casefoldedAccount) || restrictedSkeletons.Has(skeleton) {
return errAccountAlreadyRegistered
}
config := am.server.Config()
// final "is registration allowed" check:
if !(config.Accounts.Registration.Enabled || callbackNamespace == "admin") || am.server.Defcon() <= 4 {
return errFeatureDisabled
}
if client != nil && client.Account() != "" {
return errAccountAlreadyLoggedIn
}
if client != nil && am.touchRegisterThrottle() {
am.server.logger.Warning("accounts", "global registration throttle exceeded by client", client.Nick())
return errLimitExceeded
}
// if nick reservation is enabled, don't let people reserve nicknames
// that they would not be eligible to take, e.g.,
// 1. a nickname that someone else is currently holding
// 2. a nickname confusable with an existing reserved nickname
// this has a lot of weird edge cases because of force-guest-format
// and the possibility of registering a nickname on an "unregistered connection"
// (i.e., pre-handshake).
if client != nil && config.Accounts.NickReservation.Enabled {
_, nickAcquireError, _ := am.server.clients.SetNick(client, nil, account, true)
if !(nickAcquireError == nil || nickAcquireError == errNoop) {
return errAccountMustHoldNick
}
}
// can't register a guest nickname
if config.Accounts.NickReservation.guestRegexpFolded.MatchString(casefoldedAccount) {
return errAccountAlreadyRegistered
}
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
var creds AccountCredentials
creds.Version = 1
err = creds.SetPassphrase(passphrase, am.server.Config().Accounts.Registration.BcryptCost)
if err != nil {
return err
}
creds.AddCertfp(certfp)
credStr, err := creds.Serialize()
if err != nil {
return err
}
registeredTimeStr := strconv.FormatInt(time.Now().UnixNano(), 10)
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
var setOptions *buntdb.SetOptions
ttl := time.Duration(config.Accounts.Registration.VerifyTimeout)
if ttl != 0 {
setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
}
err = func() error {
am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock()
// can't register an account with the same name as a registered nick
if am.NickToAccount(account) != "" {
return errAccountAlreadyRegistered
}
return am.server.store.Update(func(tx *buntdb.Tx) error {
if _, err := tx.Get(unregisteredKey); err == nil {
return errAccountAlreadyUnregistered
}
_, err = am.loadRawAccount(tx, casefoldedAccount)
if err != errAccountDoesNotExist {
return errAccountAlreadyRegistered
}
if certfp != "" {
// make sure certfp doesn't already exist because that'd be silly
_, err := tx.Get(certFPKey)
if err != buntdb.ErrNotFound {
return errCertfpAlreadyExists
}
}
tx.Set(accountKey, "1", setOptions)
tx.Set(accountNameKey, account, setOptions)
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
tx.Set(credentialsKey, credStr, setOptions)
tx.Set(callbackKey, callbackSpec, setOptions)
if certfp != "" {
tx.Set(certFPKey, casefoldedAccount, setOptions)
}
return nil
})
}()
if err != nil {
return err
}
code, err := am.dispatchCallback(client, account, callbackNamespace, callbackValue)
if err != nil {
am.Unregister(casefoldedAccount, true)
return errCallbackFailed
} else {
return am.server.store.Update(func(tx *buntdb.Tx) error {
_, _, err = tx.Set(verificationCodeKey, code, setOptions)
return err
})
}
}
// validatePassphrase checks whether a passphrase is allowed by our rules
func validatePassphrase(passphrase string) error {
// sanity check the length
if len(passphrase) == 0 || len(passphrase) > 300 {
return errAccountBadPassphrase
}
// we use * as a placeholder in some places, if it's gotten this far then fail
if passphrase == "*" {
return errAccountBadPassphrase
}
// validate that the passphrase contains no spaces, and furthermore is valid as a
// non-final IRC parameter. we already checked that it is nonempty:
if passphrase[0] == ':' {
return errAccountBadPassphrase
}
for _, r := range passphrase {
if unicode.IsSpace(r) {
return errAccountBadPassphrase
}
}
return nil
}
// changes the password for an account
func (am *AccountManager) setPassword(account string, password string, hasPrivs bool) (err error) {
cfAccount, err := CasefoldName(account)
if err != nil {
return errAccountDoesNotExist
}
credKey := fmt.Sprintf(keyAccountCredentials, cfAccount)
var credStr string
am.server.store.View(func(tx *buntdb.Tx) error {
// no need to check verification status here or below;
// you either need to be auth'ed to the account or be an oper to do this
credStr, err = tx.Get(credKey)
return nil
})
if err != nil {
return errAccountDoesNotExist
}
var creds AccountCredentials
err = json.Unmarshal([]byte(credStr), &creds)
if err != nil {
return err
}
if !hasPrivs && creds.Empty() {
return errCredsExternallyManaged
}
err = creds.SetPassphrase(password, am.server.Config().Accounts.Registration.BcryptCost)
if err != nil {
return err
}
if creds.Empty() && !hasPrivs {
return errEmptyCredentials
}
newCredStr, err := creds.Serialize()
if err != nil {
return err
}
err = am.server.store.Update(func(tx *buntdb.Tx) error {
curCredStr, err := tx.Get(credKey)
if credStr != curCredStr {
return errCASFailed
}
_, _, err = tx.Set(credKey, newCredStr, nil)
return err
})
return err
}
type alwaysOnChannelStatus struct {
Modes string
JoinTime int64
}
func (am *AccountManager) saveChannels(account string, channelToModes map[string]alwaysOnChannelStatus) {
j, err := json.Marshal(channelToModes)
if err != nil {
am.server.logger.Error("internal", "couldn't marshal channel-to-modes", account, err.Error())
return
}
jStr := string(j)
key := fmt.Sprintf(keyAccountChannelToModes, account)
am.server.store.Update(func(tx *buntdb.Tx) error {
tx.Set(key, jStr, nil)
return nil
})
}
func (am *AccountManager) loadChannels(account string) (channelToModes map[string]alwaysOnChannelStatus) {
key := fmt.Sprintf(keyAccountChannelToModes, account)
var channelsStr string
am.server.store.View(func(tx *buntdb.Tx) error {
channelsStr, _ = tx.Get(key)
return nil
})
if channelsStr == "" {
return nil
}
err := json.Unmarshal([]byte(channelsStr), &channelToModes)
if err != nil {
am.server.logger.Error("internal", "couldn't marshal channel-to-modes", account, err.Error())
return nil
}
return
}
func (am *AccountManager) saveModes(account string, uModes modes.Modes) {
modeStr := uModes.String()
key := fmt.Sprintf(keyAccountModes, account)
am.server.store.Update(func(tx *buntdb.Tx) error {
tx.Set(key, modeStr, nil)
return nil
})
}
func (am *AccountManager) loadModes(account string) (uModes modes.Modes) {
key := fmt.Sprintf(keyAccountModes, account)
var modeStr string
am.server.store.View(func(tx *buntdb.Tx) error {
modeStr, _ = tx.Get(key)
return nil
})
for _, m := range modeStr {
uModes = append(uModes, modes.Mode(m))
}
return
}
func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.Time) {
key := fmt.Sprintf(keyAccountLastSeen, account)
var val string
if len(lastSeen) != 0 {
text, _ := json.Marshal(lastSeen)
val = string(text)
}
err := am.server.store.Update(func(tx *buntdb.Tx) error {
if val != "" {
tx.Set(key, val, nil)
} else {
tx.Delete(key)
}
return nil
})
if err != nil {
am.server.logger.Error("internal", "error persisting lastSeen", account, err.Error())
}
}
func (am *AccountManager) loadLastSeen(account string) (lastSeen map[string]time.Time) {
key := fmt.Sprintf(keyAccountLastSeen, account)
var lsText string
am.server.store.Update(func(tx *buntdb.Tx) error {
lsText, _ = tx.Get(key)
return nil
})
if lsText == "" {
return nil
}
err := json.Unmarshal([]byte(lsText), &lastSeen)
if err != nil {
return nil
}
return
}
func (am *AccountManager) saveRealname(account string, realname string) {
key := fmt.Sprintf(keyAccountRealname, account)
am.server.store.Update(func(tx *buntdb.Tx) error {
if realname != "" {
tx.Set(key, realname, nil)
} else {
tx.Delete(key)
}
return nil
})
}
func (am *AccountManager) loadRealname(account string) (realname string) {
key := fmt.Sprintf(keyAccountRealname, account)
am.server.store.Update(func(tx *buntdb.Tx) error {
realname, _ = tx.Get(key)
return nil
})
return
}
func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
certfp, err = utils.NormalizeCertfp(certfp)
if err != nil {
return err
}
cfAccount, err := CasefoldName(account)
if err != nil {
return errAccountDoesNotExist
}
credKey := fmt.Sprintf(keyAccountCredentials, cfAccount)
var credStr string
am.server.store.View(func(tx *buntdb.Tx) error {
credStr, err = tx.Get(credKey)
return nil
})
if err != nil {
return errAccountDoesNotExist
}
var creds AccountCredentials
err = json.Unmarshal([]byte(credStr), &creds)
if err != nil {
return err
}
if !hasPrivs && creds.Empty() {
return errCredsExternallyManaged
}
if add {
err = creds.AddCertfp(certfp)
} else {
err = creds.RemoveCertfp(certfp)
}
if err != nil {
return err
}
if creds.Empty() && !hasPrivs {
return errEmptyCredentials
}
newCredStr, err := creds.Serialize()
if err != nil {
return err
}
certfpKey := fmt.Sprintf(keyCertToAccount, certfp)
err = am.server.store.Update(func(tx *buntdb.Tx) error {
curCredStr, err := tx.Get(credKey)
if credStr != curCredStr {
return errCASFailed
}
if add {
_, err = tx.Get(certfpKey)
if err != buntdb.ErrNotFound {
return errCertfpAlreadyExists
}
tx.Set(certfpKey, cfAccount, nil)
} else {
tx.Delete(certfpKey)
}
_, _, err = tx.Set(credKey, newCredStr, nil)
return err
})
return err
}
func (am *AccountManager) dispatchCallback(client *Client, account string, callbackNamespace string, callbackValue string) (string, error) {
if callbackNamespace == "*" || callbackNamespace == "none" || callbackNamespace == "admin" {
return "", nil
} else if callbackNamespace == "mailto" {
return am.dispatchMailtoCallback(client, account, callbackValue)
} else {
return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace)
}
}
func (am *AccountManager) dispatchMailtoCallback(client *Client, account string, callbackValue string) (code string, err error) {
config := am.server.Config().Accounts.Registration.EmailVerification
code = utils.GenerateSecretToken()
subject := config.VerifyMessageSubject
if subject == "" {
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
}
var message bytes.Buffer
fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
fmt.Fprintf(&message, "To: %s\r\n", callbackValue)
if config.DKIM.Domain != "" {
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), config.DKIM.Domain)
}
fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
fmt.Fprintf(&message, "Subject: %s\r\n", subject)
message.WriteString("\r\n") // blank line: end headers, begin message body
fmt.Fprintf(&message, client.t("Account: %s"), account)
message.WriteString("\r\n")
fmt.Fprintf(&message, client.t("Verification code: %s"), code)
message.WriteString("\r\n")
message.WriteString("\r\n")
message.WriteString(client.t("To verify your account, issue the following command:"))
message.WriteString("\r\n")
fmt.Fprintf(&message, "/MSG NickServ VERIFY %s %s\r\n", account, code)
err = email.SendMail(config, callbackValue, message.Bytes())
if err != nil {
am.server.logger.Error("internal", "Failed to dispatch e-mail to", callbackValue, err.Error())
}
return
}
func (am *AccountManager) Verify(client *Client, account string, code string) error {
casefoldedAccount, err := CasefoldName(account)
var skeleton string
if err != nil || account == "" || account == "*" {
return errAccountVerificationFailed
}
if client != nil && client.Account() != "" {
return errAccountAlreadyLoggedIn
}
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
var raw rawClientAccount
func() {
am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock()
// do a final check for confusability (in case someone already verified
// a confusable identifier):
var unfoldedName string
err = am.server.store.View(func(tx *buntdb.Tx) error {
unfoldedName, err = tx.Get(accountNameKey)
return err
})
if err != nil {
err = errAccountDoesNotExist
return
}
skeleton, err = Skeleton(unfoldedName)
if err != nil {
err = errAccountDoesNotExist
return
}
err = func() error {
am.RLock()
defer am.RUnlock()
if _, ok := am.skeletonToAccount[skeleton]; ok {
return errConfusableIdentifier
}
return nil
}()
if err != nil {
return
}
err = am.server.store.Update(func(tx *buntdb.Tx) error {
raw, err = am.loadRawAccount(tx, casefoldedAccount)
if err == errAccountDoesNotExist {
return errAccountDoesNotExist
} else if err != nil {
return errAccountVerificationFailed
} else if raw.Verified {
return errAccountAlreadyVerified
}
// actually verify the code
// a stored code of "" means a none callback / no code required
success := false
storedCode, err := tx.Get(verificationCodeKey)
if err == nil {
// this is probably unnecessary
if storedCode == "" || utils.SecretTokensMatch(storedCode, code) {
success = true
}
}
if !success {
return errAccountVerificationInvalidCode
}
// verify the account
tx.Set(verifiedKey, "1", nil)
// don't need the code anymore
tx.Delete(verificationCodeKey)
// re-set all other keys, removing the TTL
tx.Set(accountKey, "1", nil)
tx.Set(accountNameKey, raw.Name, nil)
tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
tx.Set(callbackKey, raw.Callback, nil)
tx.Set(credentialsKey, raw.Credentials, nil)
var creds AccountCredentials
// XXX we shouldn't do (de)serialization inside the txn,
// but this is like 2 usec on my system
json.Unmarshal([]byte(raw.Credentials), &creds)
for _, cert := range creds.Certfps {
certFPKey := fmt.Sprintf(keyCertToAccount, cert)
tx.Set(certFPKey, casefoldedAccount, nil)
}
return nil
})
if err == nil {
am.Lock()
am.nickToAccount[casefoldedAccount] = casefoldedAccount
am.skeletonToAccount[skeleton] = casefoldedAccount
am.Unlock()
}
}()
if err != nil {
return err
}
nick := "[server admin]"
if client != nil {
nick = client.Nick()
}
am.server.logger.Info("accounts", "client", nick, "registered account", account)
raw.Verified = true
clientAccount, err := am.deserializeRawAccount(raw, casefoldedAccount)
if err != nil {
return err
}
if client != nil {
am.Login(client, clientAccount)
if client.AlwaysOn() {
client.markDirty(IncludeRealname)
}
}
// we may need to do nick enforcement here:
_, method := am.EnforcementStatus(casefoldedAccount, skeleton)
if method == NickEnforcementStrict {
currentClient := am.server.clients.Get(casefoldedAccount)
if currentClient != nil && currentClient != client && currentClient.Account() != casefoldedAccount {
am.server.RandomlyRename(currentClient)
}
}
return nil
}
// register and verify an account, for internal use
func (am *AccountManager) SARegister(account, passphrase string) (err error) {
err = am.Register(nil, account, "admin", "", passphrase, "")
if err == nil {
err = am.Verify(nil, account, "")
}
return
}
func marshalReservedNicks(nicks []string) string {
return strings.Join(nicks, ",")
}
func unmarshalReservedNicks(nicks string) (result []string) {
if nicks == "" {
return
}
return strings.Split(nicks, ",")
}
func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreserve bool, reserve bool) error {
cfnick, err := CasefoldName(nick)
skeleton, skerr := Skeleton(nick)
// garbage nick, or garbage options, or disabled
nrconfig := am.server.Config().Accounts.NickReservation
if err != nil || skerr != nil || cfnick == "" || (reserve && saUnreserve) || !nrconfig.Enabled {
return errAccountNickReservationFailed
}
// the cache is in sync with the DB while we hold serialCacheUpdateMutex
am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock()
// find the affected account, which is usually the client's:
account := client.Account()
if saUnreserve {
// unless this is a sadrop:
account := func() string {
am.RLock()
defer am.RUnlock()
return am.nickToAccount[cfnick]
}()
if account == "" {
// nothing to do
return nil
}
}
if account == "" {
return errAccountNotLoggedIn
}
am.Lock()
accountForNick := am.nickToAccount[cfnick]
var accountForSkeleton string
if reserve {
accountForSkeleton = am.skeletonToAccount[skeleton]
}
am.Unlock()
if reserve && (accountForNick != "" || accountForSkeleton != "") {
return errNicknameReserved
} else if !reserve && !saUnreserve && accountForNick != account {
return errNicknameReserved
} else if !reserve && cfnick == account {
return errAccountCantDropPrimaryNick
}
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, account)
unverifiedAccountKey := fmt.Sprintf(keyAccountExists, cfnick)
err = am.server.store.Update(func(tx *buntdb.Tx) error {
if reserve {
// unverified accounts don't show up in NickToAccount yet (which is intentional),
// however you shouldn't be able to reserve a nick out from under them
_, err := tx.Get(unverifiedAccountKey)
if err == nil {
return errNicknameReserved
}
}
rawNicks, err := tx.Get(nicksKey)
if err != nil && err != buntdb.ErrNotFound {
return err
}
nicks := unmarshalReservedNicks(rawNicks)
if reserve {
if len(nicks) >= nrconfig.AdditionalNickLimit {
return errAccountTooManyNicks
}
nicks = append(nicks, nick)
} else {
// compute (original reserved nicks) minus cfnick
var newNicks []string
for _, reservedNick := range nicks {
cfreservednick, _ := CasefoldName(reservedNick)
if cfreservednick != cfnick {
newNicks = append(newNicks, reservedNick)
} else {
// found the original, unfolded version of the nick we're dropping;
// recompute the true skeleton from it
skeleton, _ = Skeleton(reservedNick)
}
}
nicks = newNicks
}
marshaledNicks := marshalReservedNicks(nicks)
_, _, err = tx.Set(nicksKey, string(marshaledNicks), nil)
return err
})
if err == errAccountTooManyNicks || err == errNicknameReserved {
return err
} else if err != nil {
return errAccountNickReservationFailed
}
// success
am.Lock()
defer am.Unlock()
if reserve {
am.nickToAccount[cfnick] = account
am.skeletonToAccount[skeleton] = account
} else {
delete(am.nickToAccount, cfnick)
delete(am.skeletonToAccount, skeleton)
}
return nil
}
func (am *AccountManager) checkPassphrase(accountName, passphrase string) (account ClientAccount, err error) {
account, err = am.LoadAccount(accountName)
// #1476: if grouped nicks are allowed, attempt to interpret accountName as a grouped nick
if err == errAccountDoesNotExist && !am.server.Config().Accounts.NickReservation.ForceNickEqualsAccount {
cfnick, cfErr := CasefoldName(accountName)
if cfErr != nil {
return
}
accountName = func() string {
am.RLock()
defer am.RUnlock()
return am.nickToAccount[cfnick]
}()
if accountName != "" {
account, err = am.LoadAccount(accountName)
}
}
if err != nil {
return
}
if !account.Verified {
err = errAccountUnverified
return
} else if account.Suspended != nil {
err = errAccountSuspended
return
}
switch account.Credentials.Version {
case 0:
err = am.checkLegacyPassphrase(migrations.CheckOragonoPassphraseV0, accountName, account.Credentials.PassphraseHash, passphrase)
case 1:
if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil {
err = errAccountInvalidCredentials
}
case -1:
err = am.checkLegacyPassphrase(migrations.CheckAthemePassphrase, accountName, account.Credentials.PassphraseHash, passphrase)
case -2:
err = am.checkLegacyPassphrase(migrations.CheckAnopePassphrase, accountName, account.Credentials.PassphraseHash, passphrase)
default:
err = errAccountInvalidCredentials
}
return
}
func (am *AccountManager) checkLegacyPassphrase(check migrations.PassphraseCheck, account string, hash []byte, passphrase string) (err error) {
err = check(hash, []byte(passphrase))
if err != nil {
if err == migrations.ErrHashInvalid {
am.server.logger.Error("internal", "invalid legacy credentials for account", account)
}
return errAccountInvalidCredentials
}
// re-hash the passphrase with the latest algorithm
err = am.setPassword(account, passphrase, true)
if err != nil {
am.server.logger.Error("internal", "could not upgrade user password", err.Error())
}
return nil
}
func (am *AccountManager) loadWithAutocreation(accountName string, autocreate bool) (account ClientAccount, err error) {
account, err = am.LoadAccount(accountName)
if err == errAccountDoesNotExist && autocreate {
err = am.SARegister(accountName, "")
if err != nil {
return
}
account, err = am.LoadAccount(accountName)
}
return
}
func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) (err error) {
// XXX check this now, so we don't allow a redundant login for an always-on client
// even for a brief period. the other potential source of nick-account conflicts
// is from force-nick-equals-account, but those will be caught later by
// fixupNickEqualsAccount and if there is a conflict, they will be logged out.
if client.registered {
if clientAlready := am.server.clients.Get(accountName); clientAlready != nil && clientAlready.AlwaysOn() {
return errNickAccountMismatch
}
}
if throttled, remainingTime := client.checkLoginThrottle(); throttled {
return &ThrottleError{remainingTime}
}
var account ClientAccount
defer func() {
if err == nil {
am.Login(client, account)
}
}()
config := am.server.Config()
if config.Accounts.AuthScript.Enabled {
var output AuthScriptOutput
output, err = CheckAuthScript(am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig,
AuthScriptInput{AccountName: accountName, Passphrase: passphrase, IP: client.IP().String()})
if err != nil {
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
} else if output.Success {
if output.AccountName != "" {
accountName = output.AccountName
}
account, err = am.loadWithAutocreation(accountName, config.Accounts.AuthScript.Autocreate)
return
}
}
account, err = am.checkPassphrase(accountName, passphrase)
return err
}
// AllNicks returns the uncasefolded nicknames for all accounts, including additional (grouped) nicks.
func (am *AccountManager) AllNicks() (result []string) {
accountNamePrefix := fmt.Sprintf(keyAccountName, "")
accountAdditionalNicksPrefix := fmt.Sprintf(keyAccountAdditionalNicks, "")
am.server.store.View(func(tx *buntdb.Tx) error {
// Account names
err := tx.AscendGreaterOrEqual("", accountNamePrefix, func(key, value string) bool {
if !strings.HasPrefix(key, accountNamePrefix) {
return false
}
result = append(result, value)
return true
})
if err != nil {
return err
}
// Additional nicks
return tx.AscendGreaterOrEqual("", accountAdditionalNicksPrefix, func(key, value string) bool {
if !strings.HasPrefix(key, accountAdditionalNicksPrefix) {
return false
}
additionalNicks := unmarshalReservedNicks(value)
for _, additionalNick := range additionalNicks {
result = append(result, additionalNick)
}
return true
})
})
sort.Strings(result)
return
}
func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {
casefoldedAccount, err := CasefoldName(accountName)
if err != nil {
err = errAccountDoesNotExist
return
}
var raw rawClientAccount
am.server.store.View(func(tx *buntdb.Tx) error {
raw, err = am.loadRawAccount(tx, casefoldedAccount)
return nil
})
if err != nil {
return
}
result, err = am.deserializeRawAccount(raw, casefoldedAccount)
return
}
// look up the unfolded version of an account name, possibly after deletion
func (am *AccountManager) AccountToAccountName(account string) (result string) {
casefoldedAccount, err := CasefoldName(account)
if err != nil {
return
}
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
am.server.store.View(func(tx *buntdb.Tx) error {
if name, err := tx.Get(accountNameKey); err == nil {
result = name
return nil
}
if name, err := tx.Get(unregisteredKey); err == nil {
result = name
}
return nil
})
return
}
func (am *AccountManager) deserializeRawAccount(raw rawClientAccount, cfName string) (result ClientAccount, err error) {
result.Name = raw.Name
result.NameCasefolded = cfName
regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
result.RegisteredAt = time.Unix(0, regTimeInt).UTC()
e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
if e != nil {
am.server.logger.Error("internal", "could not unmarshal credentials", e.Error())
err = errAccountDoesNotExist
return
}
result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
if strings.HasPrefix(raw.Callback, "mailto:") {
result.Email = strings.TrimPrefix(raw.Callback, "mailto:")
}
result.Verified = raw.Verified
if raw.VHost != "" {
e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
if e != nil {
am.server.logger.Warning("internal", "could not unmarshal vhost for account", result.Name, e.Error())
// pretend they have no vhost and move on
}
}
if raw.Settings != "" {
e := json.Unmarshal([]byte(raw.Settings), &result.Settings)
if e != nil {
am.server.logger.Warning("internal", "could not unmarshal settings for account", result.Name, e.Error())
}
}
if raw.Suspended != "" {
sus := new(AccountSuspension)
e := json.Unmarshal([]byte(raw.Suspended), sus)
if e != nil {
am.server.logger.Error("internal", "corrupt suspension data", result.Name, e.Error())
} else {
result.Suspended = sus
}
}
return
}
func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string) (result rawClientAccount, err error) {
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
_, e := tx.Get(accountKey)
if e == buntdb.ErrNotFound {
err = errAccountDoesNotExist
return
}
result.Name, _ = tx.Get(accountNameKey)
result.RegisteredAt, _ = tx.Get(registeredTimeKey)
result.Credentials, _ = tx.Get(credentialsKey)
result.Callback, _ = tx.Get(callbackKey)
result.AdditionalNicks, _ = tx.Get(nicksKey)
result.VHost, _ = tx.Get(vhostKey)
result.Settings, _ = tx.Get(settingsKey)
result.Suspended, _ = tx.Get(suspendedKey)
if _, e = tx.Get(verifiedKey); e == nil {
result.Verified = true
}
return
}
type AccountSuspension struct {
AccountName string `json:"AccountName,omitempty"`
TimeCreated time.Time
Duration time.Duration
OperName string
Reason string
}
func (am *AccountManager) Suspend(accountName string, duration time.Duration, operName, reason string) (err error) {
account, err := CasefoldName(accountName)
if err != nil {
return errAccountDoesNotExist
}
suspension := AccountSuspension{
TimeCreated: time.Now().UTC(),
Duration: duration,
OperName: operName,
Reason: reason,
}
suspensionStr, err := json.Marshal(suspension)
if err != nil {
am.server.logger.Error("internal", "suspension json unserializable", err.Error())
return errAccountDoesNotExist
}
existsKey := fmt.Sprintf(keyAccountExists, account)
suspensionKey := fmt.Sprintf(keyAccountSuspended, account)
var setOptions *buntdb.SetOptions
if duration != time.Duration(0) {
setOptions = &buntdb.SetOptions{Expires: true, TTL: duration}
}
err = am.server.store.Update(func(tx *buntdb.Tx) error {
_, err := tx.Get(existsKey)
if err != nil {
return errAccountDoesNotExist
}
_, _, err = tx.Set(suspensionKey, string(suspensionStr), setOptions)
return err
})
if err == errAccountDoesNotExist {
return err
} else if err != nil {
am.server.logger.Error("internal", "couldn't persist suspension", account, err.Error())
} // keep going
am.Lock()
clients := am.accountToClients[account]
delete(am.accountToClients, account)
am.Unlock()
// kill clients, sending them the reason
suspension.AccountName = accountName
for _, client := range clients {
client.Logout()
client.Quit(suspensionToString(client, suspension), nil)
client.destroy(nil)
}
return nil
}
func (am *AccountManager) killClients(clients []*Client) {
for _, client := range clients {
client.Logout()
client.Quit(client.t("You are no longer authorized to be on this server"), nil)
client.destroy(nil)
}
}
func (am *AccountManager) Unsuspend(accountName string) (err error) {
cfaccount, err := CasefoldName(accountName)
if err != nil {
return errAccountDoesNotExist
}
existsKey := fmt.Sprintf(keyAccountExists, cfaccount)
suspensionKey := fmt.Sprintf(keyAccountSuspended, cfaccount)
err = am.server.store.Update(func(tx *buntdb.Tx) error {
_, err := tx.Get(existsKey)
if err != nil {
return errAccountDoesNotExist
}
_, err = tx.Delete(suspensionKey)
if err != nil {
return errNoop
}
return nil
})
return err
}
func (am *AccountManager) ListSuspended() (result []AccountSuspension) {
var names []string
var raw []string
prefix := fmt.Sprintf(keyAccountSuspended, "")
am.server.store.View(func(tx *buntdb.Tx) error {
err := tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
raw = append(raw, value)
cfname := strings.TrimPrefix(key, prefix)
name, _ := tx.Get(fmt.Sprintf(keyAccountName, cfname))
names = append(names, name)
return true
})
return err
})
result = make([]AccountSuspension, 0, len(raw))
for i := 0; i < len(raw); i++ {
var sus AccountSuspension
err := json.Unmarshal([]byte(raw[i]), &sus)
if err != nil {
am.server.logger.Error("internal", "corrupt data for suspension", names[i], err.Error())
continue
}
sus.AccountName = names[i]
result = append(result, sus)
}
return
}
// renames an account (within very restrictive limits); see #1380
func (am *AccountManager) Rename(oldName, newName string) (err error) {
accountData, err := am.LoadAccount(oldName)
if err != nil {
return
}
newCfName, err := CasefoldName(newName)
if err != nil {
return errNicknameInvalid
}
if newCfName != accountData.NameCasefolded {
return errInvalidAccountRename
}
key := fmt.Sprintf(keyAccountName, accountData.NameCasefolded)
err = am.server.store.Update(func(tx *buntdb.Tx) error {
tx.Set(key, newName, nil)
return nil
})
if err != nil {
return err
}
am.RLock()
defer am.RUnlock()
for _, client := range am.accountToClients[accountData.NameCasefolded] {
client.setAccountName(newName)
}
return nil
}
func (am *AccountManager) Unregister(account string, erase bool) error {
config := am.server.Config()
casefoldedAccount, err := CasefoldName(account)
if err != nil {
return errAccountDoesNotExist
}
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
var clients []*Client
defer func() {
am.killClients(clients)
}()
var registeredChannels []string
// on our way out, unregister all the account's channels and delete them from the db
defer func() {
for _, channelName := range registeredChannels {
err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
if err != nil {
am.server.logger.Error("internal", "couldn't unregister channel", channelName, err.Error())
}
}
}()
var credText string
var rawNicks string
am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock()
var accountName string
var channelsStr string
keepProtections := false
am.server.store.Update(func(tx *buntdb.Tx) error {
// get the unfolded account name; for an active account, this is
// stored under accountNameKey, for an unregistered account under unregisteredKey
accountName, _ = tx.Get(accountNameKey)
if accountName == "" {
accountName, _ = tx.Get(unregisteredKey)
}
if erase {
tx.Delete(unregisteredKey)
} else {
if _, err := tx.Get(verifiedKey); err == nil {
tx.Set(unregisteredKey, accountName, nil)
keepProtections = true
}
}
tx.Delete(accountKey)
tx.Delete(accountNameKey)
tx.Delete(verifiedKey)
tx.Delete(registeredTimeKey)
tx.Delete(callbackKey)
tx.Delete(verificationCodeKey)
tx.Delete(settingsKey)
rawNicks, _ = tx.Get(nicksKey)
tx.Delete(nicksKey)
credText, err = tx.Get(credentialsKey)
tx.Delete(credentialsKey)
tx.Delete(vhostKey)
channelsStr, _ = tx.Get(channelsKey)
tx.Delete(channelsKey)
tx.Delete(joinedChannelsKey)
tx.Delete(lastSeenKey)
tx.Delete(modesKey)
tx.Delete(realnameKey)
tx.Delete(suspendedKey)
return nil
})
if err == nil {
var creds AccountCredentials
if err := json.Unmarshal([]byte(credText), &creds); err == nil {
for _, cert := range creds.Certfps {
certFPKey := fmt.Sprintf(keyCertToAccount, cert)
am.server.store.Update(func(tx *buntdb.Tx) error {
if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
tx.Delete(certFPKey)
}
return nil
})
}
}
}
skeleton, _ := Skeleton(accountName)
additionalNicks := unmarshalReservedNicks(rawNicks)
registeredChannels = unmarshalRegisteredChannels(channelsStr)
am.Lock()
defer am.Unlock()
clients = am.accountToClients[casefoldedAccount]
delete(am.accountToClients, casefoldedAccount)
// protect the account name itself where applicable, but not any grouped nicks
if !(keepProtections && config.Accounts.NickReservation.Method == NickEnforcementStrict) {
delete(am.nickToAccount, casefoldedAccount)
delete(am.skeletonToAccount, skeleton)
}
for _, nick := range additionalNicks {
delete(am.nickToAccount, nick)
additionalSkel, _ := Skeleton(nick)
delete(am.skeletonToAccount, additionalSkel)
}
if err != nil && !erase {
return errAccountDoesNotExist
}
return nil
}
func unmarshalRegisteredChannels(channelsStr string) (result []string) {
if channelsStr != "" {
result = strings.Split(channelsStr, ",")
}
return
}
func (am *AccountManager) ChannelsForAccount(account string) (channels []string) {
cfaccount, err := CasefoldName(account)
if err != nil {
return
}
var channelStr string
key := fmt.Sprintf(keyAccountChannels, cfaccount)
am.server.store.View(func(tx *buntdb.Tx) error {
channelStr, _ = tx.Get(key)
return nil
})
return unmarshalRegisteredChannels(channelStr)
}
func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
if certfp == "" {
return errAccountInvalidCredentials
}
var clientAccount ClientAccount
defer func() {
if err != nil {
return
} else if !clientAccount.Verified {
err = errAccountUnverified
return
} else if clientAccount.Suspended != nil {
err = errAccountSuspended
return
}
// TODO(#1109) clean this check up?
if client.registered {
if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() {
err = errNickAccountMismatch
return
}
}
am.Login(client, clientAccount)
return
}()
config := am.server.Config()
if config.Accounts.AuthScript.Enabled {
var output AuthScriptOutput
output, err = CheckAuthScript(am.server.semaphores.AuthScript, config.Accounts.AuthScript.ScriptConfig,
AuthScriptInput{Certfp: certfp, IP: client.IP().String(), peerCerts: peerCerts})
if err != nil {
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
} else if output.Success && output.AccountName != "" {
clientAccount, err = am.loadWithAutocreation(output.AccountName, config.Accounts.AuthScript.Autocreate)
return
}
}
var account string
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
err = am.server.store.View(func(tx *buntdb.Tx) error {
account, _ = tx.Get(certFPKey)
if account == "" {
return errAccountInvalidCredentials
}
return nil
})
if err != nil {
return err
}
if authzid != "" && authzid != account {
return errAuthzidAuthcidMismatch
}
// ok, we found an account corresponding to their certificate
clientAccount, err = am.LoadAccount(account)
return err
}
type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
func (am *AccountManager) ModifyAccountSettings(account string, munger settingsMunger) (newSettings AccountSettings, err error) {
casefoldedAccount, err := CasefoldName(account)
if err != nil {
return newSettings, errAccountDoesNotExist
}
// TODO implement this in general via a compare-and-swap API
accountData, err := am.LoadAccount(casefoldedAccount)
if err != nil {
return
} else if !accountData.Verified {
return newSettings, errAccountUnverified
}
newSettings, err = munger(accountData.Settings)
if err != nil {
return
}
text, err := json.Marshal(newSettings)
if err != nil {
return
}
key := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
serializedValue := string(text)
err = am.server.store.Update(func(tx *buntdb.Tx) (err error) {
_, _, err = tx.Set(key, serializedValue, nil)
return
})
if err != nil {
err = errAccountUpdateFailed
return
}
// success, push new settings into the client objects
am.Lock()
defer am.Unlock()
for _, client := range am.accountToClients[casefoldedAccount] {
client.SetAccountSettings(newSettings)
}
return
}
// represents someone's status in hostserv
type VHostInfo struct {
ApprovedVHost string
Enabled bool
}
// callback type implementing the actual business logic of vhost operations
type vhostMunger func(input VHostInfo) (output VHostInfo, err error)
func (am *AccountManager) VHostSet(account string, vhost string) (result VHostInfo, err error) {
munger := func(input VHostInfo) (output VHostInfo, err error) {
output = input
output.Enabled = true
output.ApprovedVHost = vhost
return
}
return am.performVHostChange(account, munger)
}
func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result VHostInfo, err error) {
munger := func(input VHostInfo) (output VHostInfo, err error) {
if input.ApprovedVHost == "" {
err = errNoVhost
return
}
output = input
output.Enabled = enabled
return
}
return am.performVHostChange(client.Account(), munger)
}
func (am *AccountManager) performVHostChange(account string, munger vhostMunger) (result VHostInfo, err error) {
account, err = CasefoldName(account)
if err != nil || account == "" {
err = errAccountDoesNotExist
return
}
if am.server.Defcon() <= 3 {
err = errFeatureDisabled
return
}
clientAccount, err := am.LoadAccount(account)
if err != nil {
err = errAccountDoesNotExist
return
} else if !clientAccount.Verified {
err = errAccountUnverified
return
}
result, err = munger(clientAccount.VHost)
if err != nil {
return
}
vhtext, err := json.Marshal(result)
if err != nil {
err = errAccountUpdateFailed
return
}
vhstr := string(vhtext)
key := fmt.Sprintf(keyAccountVHost, account)
err = am.server.store.Update(func(tx *buntdb.Tx) error {
_, _, err := tx.Set(key, vhstr, nil)
return err
})
if err != nil {
err = errAccountUpdateFailed
return
}
am.applyVhostToClients(account, result)
return result, nil
}
func (am *AccountManager) applyVHostInfo(client *Client, info VHostInfo) {
// if hostserv is disabled in config, then don't grant vhosts
// that were previously approved while it was enabled
if !am.server.Config().Accounts.VHosts.Enabled {
return
}
vhost := ""
if info.Enabled {
vhost = info.ApprovedVHost
}
oldNickmask := client.NickMaskString()
updated := client.SetVHost(vhost)
if updated && client.Registered() {
// TODO: doing I/O here is kind of a kludge
client.sendChghost(oldNickmask, client.Hostname())
}
}
func (am *AccountManager) applyVhostToClients(account string, result VHostInfo) {
am.RLock()
clients := am.accountToClients[account]
am.RUnlock()
for _, client := range clients {
am.applyVHostInfo(client, result)
}
}
func (am *AccountManager) Login(client *Client, account ClientAccount) {
client.Login(account)
am.applyVHostInfo(client, account.VHost)
casefoldedAccount := client.Account()
am.Lock()
defer am.Unlock()
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
}
func (am *AccountManager) Logout(client *Client) {
am.Lock()
defer am.Unlock()
casefoldedAccount := client.Account()
if casefoldedAccount == "" {
return
}
client.Logout()
clients := am.accountToClients[casefoldedAccount]
if len(clients) <= 1 {
delete(am.accountToClients, casefoldedAccount)
return
}
remainingClients := make([]*Client, len(clients)-1)
remainingPos := 0
for currentPos := 0; currentPos < len(clients); currentPos++ {
if clients[currentPos] != client {
remainingClients[remainingPos] = clients[currentPos]
remainingPos++
}
}
am.accountToClients[casefoldedAccount] = remainingClients
}
var (
// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support.
// This can be moved to some other data structure/place if we need to load/unload mechs later.
EnabledSaslMechanisms = map[string]func(*Server, *Client, string, []byte, *ResponseBuffer) bool{
"PLAIN": authPlainHandler,
"EXTERNAL": authExternalHandler,
}
)
type CredentialsVersion int
const (
CredentialsLegacy CredentialsVersion = 0
CredentialsSHA3Bcrypt CredentialsVersion = 1
// negative numbers for migration
CredentialsAtheme = -1
CredentialsAnope = -2
)
// AccountCredentials stores the various methods for verifying accounts.
type AccountCredentials struct {
Version CredentialsVersion
PassphraseHash []byte
Certfps []string
}
func (ac *AccountCredentials) Empty() bool {
return len(ac.PassphraseHash) == 0 && len(ac.Certfps) == 0
}
// helper to assemble the serialized JSON for an account's credentials
func (ac *AccountCredentials) Serialize() (result string, err error) {
ac.Version = 1
credText, err := json.Marshal(*ac)
if err != nil {
return "", err
}
return string(credText), nil
}
func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint) (err error) {
if passphrase == "" {
ac.PassphraseHash = nil
return nil
}
if validatePassphrase(passphrase) != nil {
return errAccountBadPassphrase
}
ac.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), int(bcryptCost))
if err != nil {
return errAccountBadPassphrase
}
return nil
}
func (ac *AccountCredentials) AddCertfp(certfp string) (err error) {
// XXX we require that certfp is already normalized (rather than normalize here
// and pass back the normalized version as an additional return parameter);
// this is just a final sanity check:
if len(certfp) != 64 {
return utils.ErrInvalidCertfp
}
for _, current := range ac.Certfps {
if certfp == current {
return errNoop
}
}
if maxCertfpsPerAccount <= len(ac.Certfps) {
return errLimitExceeded
}
ac.Certfps = append(ac.Certfps, certfp)
return nil
}
func (ac *AccountCredentials) RemoveCertfp(certfp string) (err error) {
found := false
newList := make([]string, 0, len(ac.Certfps))
for _, current := range ac.Certfps {
if current == certfp {
found = true
} else {
newList = append(newList, current)
}
}
if !found {
// this is important because it prevents you from deleting someone else's
// fingerprint record
return errNoop
}
ac.Certfps = newList
return nil
}
type MulticlientAllowedSetting int
const (
MulticlientAllowedServerDefault MulticlientAllowedSetting = iota
MulticlientDisallowedByUser
MulticlientAllowedByUser
)
// controls whether/when clients without event-playback support see fake
// PRIVMSGs for JOINs
type ReplayJoinsSetting uint
const (
ReplayJoinsCommandsOnly = iota // replay in HISTORY or CHATHISTORY output
ReplayJoinsAlways // replay in HISTORY, CHATHISTORY, or autoreplay
ReplayJoinsNever // never replay
)
func replayJoinsSettingFromString(str string) (result ReplayJoinsSetting, err error) {
switch strings.ToLower(str) {
case "commands-only":
result = ReplayJoinsCommandsOnly
case "always":
result = ReplayJoinsAlways
case "never":
result = ReplayJoinsNever
default:
err = errInvalidParams
}
return
}
// XXX: AllowBouncer cannot be renamed AllowMulticlient because it is stored in
// persistent JSON blobs in the database
type AccountSettings struct {
AutoreplayLines *int
NickEnforcement NickEnforcementMethod
AllowBouncer MulticlientAllowedSetting
ReplayJoins ReplayJoinsSetting
AlwaysOn PersistentStatus
AutoreplayMissed bool
DMHistory HistoryStatus
AutoAway PersistentStatus
}
// ClientAccount represents a user account.
type ClientAccount struct {
// Name of the account.
Name string
NameCasefolded string
RegisteredAt time.Time
Email string
Credentials AccountCredentials
Verified bool
Suspended *AccountSuspension
AdditionalNicks []string
VHost VHostInfo
Settings AccountSettings
}
// convenience for passing around raw serialized account data
type rawClientAccount struct {
Name string
RegisteredAt string
Credentials string
Callback string
Verified bool
AdditionalNicks string
VHost string
Settings string
Suspended string
}