// Copyright (c) 2016-2017 Daniel Oaks // released under the MIT license package irc import ( "encoding/json" "fmt" "net/smtp" "strconv" "strings" "sync" "sync/atomic" "time" "unicode" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/ldap" "github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/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 keyAccountJoinedChannels = "account.joinedto %s" // channels a persistent client has joined keyAccountLastSeen = "account.lastseen %s" keyVHostQueueAcctToId = "vhostQueue %s" vhostRequestIdx = "vhostQueue" 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 { // XXX these are up here so they can be aligned to a 64-bit boundary, please forgive me // autoincrementing ID for vhost requests: vhostRequestID uint64 vhostRequestPendingCount uint64 sync.RWMutex // tier 2 serialCacheUpdateMutex sync.Mutex // tier 3 vHostUpdateMutex 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 } 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.initVHostRequestQueue(config) am.createAlwaysOnClients(config) } 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 && persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn) { am.server.AddAlwaysOnClient(account, am.loadChannels(accountName), am.loadLastSeen(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) initVHostRequestQueue(config *Config) { if !config.Accounts.VHosts.Enabled { return } am.vHostUpdateMutex.Lock() defer am.vHostUpdateMutex.Unlock() // the db maps the account name to the autoincrementing integer ID of its request // create an numerically ordered index on ID, so we can list the oldest requests // finally, collect the integer id of the newest request and the total request count var total uint64 var lastIDStr string err := am.server.store.Update(func(tx *buntdb.Tx) error { err := tx.CreateIndex(vhostRequestIdx, fmt.Sprintf(keyVHostQueueAcctToId, "*"), buntdb.IndexInt) if err != nil { return err } return tx.Descend(vhostRequestIdx, func(key, value string) bool { if lastIDStr == "" { lastIDStr = value } total++ return true }) }) if err != nil { am.server.logger.Error("internal", "could not create vhost queue index", err.Error()) } lastID, _ := strconv.ParseUint(lastIDStr, 10, 64) am.server.logger.Debug("services", fmt.Sprintf("vhost queue length is %d, autoincrementing id is %d", total, lastID)) atomic.StoreUint64(&am.vhostRequestID, lastID) atomic.StoreUint64(&am.vhostRequestPendingCount, total) } func (am *AccountManager) NickToAccount(nick string) string { cfnick, err := CasefoldName(nick) if err != nil { return "" } am.RLock() defer am.RUnlock() return am.nickToAccount[cfnick] } // 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[casefoldedAccount] || restrictedSkeletons[skeleton] { return errAccountAlreadyRegistered } config := am.server.Config() // final "is registration allowed" check, probably redundant: if !(config.Accounts.Registration.Enabled || callbackNamespace == "admin") { return errFeatureDisabled } // if nick reservation is enabled, you can only register your current nickname // as an account; this prevents "land-grab" situations where someone else // registers your nick out from under you and then NS GHOSTs you // n.b. client is nil during a SAREGISTER // n.b. if ForceGuestFormat, then there's no concern, because you can't // register a guest nickname anyway, and the actual registration system // will prevent any double-register if client != nil && config.Accounts.NickReservation.Enabled && !config.Accounts.NickReservation.ForceGuestFormat && client.NickCasefolded() != casefoldedAccount { 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(casefoldedAccount) != "" { 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, casefoldedAccount, 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 } // for now, just enforce that spaces are not allowed 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 } func (am *AccountManager) saveChannels(account string, channels []string) { channelsStr := strings.Join(channels, ",") key := fmt.Sprintf(keyAccountJoinedChannels, account) am.server.store.Update(func(tx *buntdb.Tx) error { tx.Set(key, channelsStr, nil) return nil }) } func (am *AccountManager) loadChannels(account string) (channels []string) { key := fmt.Sprintf(keyAccountJoinedChannels, account) var channelsStr string am.server.store.View(func(tx *buntdb.Tx) error { channelsStr, _ = tx.Get(key) return nil }) if channelsStr != "" { return strings.Split(channelsStr, ",") } return } func (am *AccountManager) saveLastSeen(account string, lastSeen time.Time) { key := fmt.Sprintf(keyAccountLastSeen, account) var val string if !lastSeen.IsZero() { val = strconv.FormatInt(lastSeen.UnixNano(), 10) } am.server.store.Update(func(tx *buntdb.Tx) error { if val != "" { tx.Set(key, val, nil) } else { tx.Delete(key) } return nil }) } func (am *AccountManager) loadLastSeen(account string) (lastSeen time.Time) { key := fmt.Sprintf(keyAccountLastSeen, account) var lsText string am.server.store.Update(func(tx *buntdb.Tx) error { lsText, _ = tx.Get(key) // XXX clear this on startup, because it's not clear when it's // going to be overwritten, and restarting the server twice in a row // could result in a large amount of duplicated history replay tx.Delete(key) return nil }) lsNum, err := strconv.ParseInt(lsText, 10, 64) if err == nil { return time.Unix(0, lsNum).UTC() } 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, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) { if callbackNamespace == "*" || callbackNamespace == "none" || callbackNamespace == "admin" { return "", nil } else if callbackNamespace == "mailto" { return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue) } else { return "", fmt.Errorf("Callback not implemented: %s", callbackNamespace) } } func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) { config := am.server.Config().Accounts.Registration.Callbacks.Mailto code = utils.GenerateSecretToken() subject := config.VerifyMessageSubject if subject == "" { subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name) } messageStrings := []string{ fmt.Sprintf("From: %s\r\n", config.Sender), fmt.Sprintf("To: %s\r\n", callbackValue), fmt.Sprintf("Subject: %s\r\n", subject), "\r\n", // end headers, begin message body fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n", fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n", "\r\n", client.t("To verify your account, issue the following command:") + "\r\n", fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n", } var message []byte for i := 0; i < len(messageStrings); i++ { message = append(message, []byte(messageStrings[i])...) } addr := fmt.Sprintf("%s:%d", config.Server, config.Port) var auth smtp.Auth if config.Username != "" && config.Password != "" { auth = smtp.PlainAuth("", config.Username, config.Password, config.Server) } // TODO: this will never send the password in plaintext over a nonlocal link, // but it might send the email in plaintext, regardless of the value of // config.TLS.InsecureSkipVerify err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message) if err != nil { am.server.logger.Error("internal", "Failed to dispatch e-mail", err.Error()) } return } func (am *AccountManager) Verify(client *Client, account string, code string) error { casefoldedAccount, err := CasefoldName(account) if err != nil || account == "" || account == "*" { return errAccountVerificationFailed } 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() 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 { skeleton, _ := Skeleton(raw.Name) 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", casefoldedAccount) raw.Verified = true clientAccount, err := am.deserializeRawAccount(raw, casefoldedAccount) if err != nil { return err } if client != nil { am.Login(client, clientAccount) } 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 = 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) if err != nil { return } if !account.Verified { err = errAccountUnverified return } switch account.Credentials.Version { case 0: err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase) case 1: if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil { err = errAccountInvalidCredentials } default: err = errAccountInvalidCredentials } 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 } } var account ClientAccount defer func() { if err == nil { am.Login(client, account) } }() ldapConf := am.server.Config().Accounts.LDAP if ldapConf.Enabled { err = ldap.CheckLDAPPassphrase(ldapConf, accountName, passphrase, am.server.logger) if err == nil { account, err = am.LoadAccount(accountName) // autocreate if necessary: if err == errAccountDoesNotExist && ldapConf.Autocreate { err = am.SARegister(accountName, "") if err != nil { return } account, err = am.LoadAccount(accountName) } return } } account, err = am.checkPassphrase(accountName, passphrase) return err } 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 } 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) 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()) } } 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) _, 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) if _, e = tx.Get(verifiedKey); e == nil { result.Verified = true } return } 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) vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount) channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount) joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount) lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount) unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount) var clients []*Client 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 { accountName, _ = tx.Get(accountNameKey) 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) _, err := tx.Delete(vhostQueueKey) am.decrementVHostQueueCount(casefoldedAccount, err) 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) } for _, client := range clients { if config.Accounts.RequireSasl.Enabled { client.Logout() client.Quit(client.t("You are no longer authorized to be on this server"), nil) // destroy acquires a semaphore so we can't call it while holding a lock go client.destroy(nil) } else { am.logoutOfAccount(client) } } 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) AuthenticateByCertFP(client *Client, certfp, authzid string) error { if certfp == "" { return errAccountInvalidCredentials } 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) if err != nil { return err } else if !clientAccount.Verified { return errAccountUnverified } if client.registered { if clientAlready := am.server.clients.Get(clientAccount.Name); clientAlready != nil && clientAlready.AlwaysOn() { return errNickAccountMismatch } } am.Login(client, clientAccount) return nil } 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 Forbidden bool RequestedVHost string RejectedVHost string RejectionReason string LastRequestTime time.Time } // pair type, type PendingVHostRequest struct { VHostInfo Account string } type vhostThrottleExceeded struct { timeRemaining time.Duration } func (vhe *vhostThrottleExceeded) Error() string { return fmt.Sprintf("Wait at least %v and try again", vhe.timeRemaining) } func (vh *VHostInfo) checkThrottle(cooldown time.Duration) (err error) { if cooldown == 0 { return nil } now := time.Now().UTC() elapsed := now.Sub(vh.LastRequestTime) if elapsed > cooldown { // success vh.LastRequestTime = now return nil } else { return &vhostThrottleExceeded{timeRemaining: cooldown - elapsed} } } // 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) VHostRequest(account string, vhost string, cooldown time.Duration) (result VHostInfo, err error) { munger := func(input VHostInfo) (output VHostInfo, err error) { output = input if input.Forbidden { err = errVhostsForbidden return } // you can update your existing request, but if you were approved or rejected, // you can't spam a new request if output.RequestedVHost == "" { err = output.checkThrottle(cooldown) } if err != nil { return } output.RequestedVHost = vhost output.RejectedVHost = "" output.RejectionReason = "" output.LastRequestTime = time.Now().UTC() return } return am.performVHostChange(account, munger) } func (am *AccountManager) VHostTake(account string, vhost string, cooldown time.Duration) (result VHostInfo, err error) { munger := func(input VHostInfo) (output VHostInfo, err error) { output = input if input.Forbidden { err = errVhostsForbidden return } // if you have a request pending, you can cancel it using take; // otherwise, you're subject to the same throttling as if you were making a request if output.RequestedVHost == "" { err = output.checkThrottle(cooldown) } if err != nil { return } output.ApprovedVHost = vhost output.RequestedVHost = "" output.RejectedVHost = "" output.RejectionReason = "" output.LastRequestTime = time.Now().UTC() return } return am.performVHostChange(account, munger) } func (am *AccountManager) VHostApprove(account string) (result VHostInfo, err error) { munger := func(input VHostInfo) (output VHostInfo, err error) { output = input output.Enabled = true output.ApprovedVHost = input.RequestedVHost output.RequestedVHost = "" output.RejectionReason = "" return } return am.performVHostChange(account, munger) } func (am *AccountManager) VHostReject(account string, reason string) (result VHostInfo, err error) { munger := func(input VHostInfo) (output VHostInfo, err error) { output = input output.RejectedVHost = output.RequestedVHost output.RequestedVHost = "" output.RejectionReason = reason 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) VHostForbid(account string, forbid bool) (result VHostInfo, err error) { munger := func(input VHostInfo) (output VHostInfo, err error) { output = input output.Forbidden = forbid return } return am.performVHostChange(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 } am.vHostUpdateMutex.Lock() defer am.vHostUpdateMutex.Unlock() 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) queueKey := fmt.Sprintf(keyVHostQueueAcctToId, account) err = am.server.store.Update(func(tx *buntdb.Tx) error { if _, _, err := tx.Set(key, vhstr, nil); err != nil { return err } // update request queue if clientAccount.VHost.RequestedVHost == "" && result.RequestedVHost != "" { id := atomic.AddUint64(&am.vhostRequestID, 1) if _, _, err = tx.Set(queueKey, strconv.FormatUint(id, 10), nil); err != nil { return err } atomic.AddUint64(&am.vhostRequestPendingCount, 1) } else if clientAccount.VHost.RequestedVHost != "" && result.RequestedVHost == "" { _, err = tx.Delete(queueKey) am.decrementVHostQueueCount(account, err) } return nil }) if err != nil { err = errAccountUpdateFailed return } am.applyVhostToClients(account, result) return result, nil } // XXX annoying helper method for keeping the queue count in sync with the DB // `err` is the buntdb error returned from deleting the queue key func (am *AccountManager) decrementVHostQueueCount(account string, err error) { if err == nil { // successfully deleted a queue entry, do a 2's complement decrement: atomic.AddUint64(&am.vhostRequestPendingCount, ^uint64(0)) } else if err != buntdb.ErrNotFound { am.server.logger.Error("internal", "buntdb dequeue error", account, err.Error()) } } func (am *AccountManager) VHostListRequests(limit int) (requests []PendingVHostRequest, total int) { am.vHostUpdateMutex.Lock() defer am.vHostUpdateMutex.Unlock() total = int(atomic.LoadUint64(&am.vhostRequestPendingCount)) prefix := fmt.Sprintf(keyVHostQueueAcctToId, "") accounts := make([]string, 0, limit) err := am.server.store.View(func(tx *buntdb.Tx) error { return tx.Ascend(vhostRequestIdx, func(key, value string) bool { accounts = append(accounts, strings.TrimPrefix(key, prefix)) return len(accounts) < limit }) }) if err != nil { am.server.logger.Error("internal", "couldn't traverse vhost queue", err.Error()) return } for _, account := range accounts { accountInfo, err := am.LoadAccount(account) if err == nil { requests = append(requests, PendingVHostRequest{ Account: account, VHostInfo: accountInfo.VHost, }) } else { am.server.logger.Error("internal", "corrupt account", account, err.Error()) } } return } 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 && !info.Forbidden { vhost = info.ApprovedVHost } oldNickmask := client.NickMaskString() updated := client.SetVHost(vhost) if updated { // TODO: doing I/O here is kind of a kludge go 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) client.nickTimer.Touch(nil) 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 } am.logoutOfAccount(client) 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, } ) // AccountCredentials stores the various methods for verifying accounts. type AccountCredentials struct { Version uint PassphraseSalt []byte // legacy field, not used by v1 and later 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 } // ClientAccount represents a user account. type ClientAccount struct { // Name of the account. Name string NameCasefolded string RegisteredAt time.Time Credentials AccountCredentials Verified bool 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 } // logoutOfAccount logs the client out of their current account. func (am *AccountManager) logoutOfAccount(client *Client) { if client.Account() == "" { // already logged out return } client.Logout() go client.nickTimer.Touch(nil) // dispatch account-notify // TODO: doing the I/O here is kind of a kludge, let's move this somewhere else go func() { for friend := range client.Friends(caps.AccountNotify) { friend.Send(nil, client.NickMaskString(), "ACCOUNT", "*") } }() }