1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353 |
- // Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
- // released under the MIT license
-
- package irc
-
- import (
- "crypto/rand"
- "crypto/x509"
- "encoding/json"
- "fmt"
- "sort"
- "strconv"
- "strings"
- "sync"
- "time"
- "unicode"
-
- "github.com/ergochat/irc-go/ircutils"
- "github.com/tidwall/buntdb"
- "github.com/xdg-go/scram"
-
- "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"
- )
-
- const (
- keyAccountExists = "account.exists %s"
- keyAccountVerified = "account.verified %s"
- keyAccountUnregistered = "account.unregistered %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
- keyAccountPwReset = "account.pwreset %s"
- keyAccountEmailChange = "account.emailchange %s"
- // 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 && rawPrefs != "" {
- 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 settings", account, err.Error())
- }
- }
-
- 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 callbackNamespace != "admin" && (!config.Accounts.Registration.Enabled || 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)
- registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
- credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
- verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
- settingsKey := fmt.Sprintf(keyAccountSettings, 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
- }
-
- var settingsStr string
- if callbackNamespace == "mailto" {
- settings := AccountSettings{Email: callbackValue}
- j, err := json.Marshal(settings)
- if err == nil {
- settingsStr = string(j)
- }
- }
-
- registeredTimeStr := strconv.FormatInt(time.Now().UnixNano(), 10)
-
- 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(settingsKey, settingsStr, 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 ®istrationCallbackError{underlying: err}
- } else {
- if client != nil && code != "" {
- am.server.logger.Info("accounts",
- fmt.Sprintf("nickname %s registered account %s, pending verification", client.Nick(), account))
- }
- return am.server.store.Update(func(tx *buntdb.Tx) error {
- _, _, err = tx.Set(verificationCodeKey, code, setOptions)
- return err
- })
- }
- }
-
- type registrationCallbackError struct {
- underlying error
- }
-
- func (r *registrationCallbackError) Error() string {
- return `Account verification could not be sent`
- }
-
- func registrationCallbackErrorText(config *Config, client *Client, err error) string {
- if callbackErr, ok := err.(*registrationCallbackError); ok {
- // only expose a user-visible error if we are doing direct sending
- if config.Accounts.Registration.EmailVerification.DirectSendingEnabled() {
- errorText := ircutils.SanitizeText(callbackErr.underlying.Error(), 350)
- return fmt.Sprintf(client.t("Could not dispatch registration e-mail: %s"), errorText)
- } else {
- return client.t("Could not dispatch registration e-mail")
- }
- } else {
- return ""
- }
- }
-
- // 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(accountName string, password string, hasPrivs bool) (err error) {
- cfAccount, err := CasefoldName(accountName)
- 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)
- }
-
- message := email.ComposeMail(config, callbackValue, subject)
- 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)
- credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
- settingsKey := fmt.Sprintf(keyAccountSettings, 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(credentialsKey, raw.Credentials, nil)
- tx.Set(settingsKey, raw.Settings, 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
- }
-
- type EmailChangeRecord struct {
- TimeCreated time.Time
- Code string
- Email string
- }
-
- func (am *AccountManager) NsSetEmail(client *Client, emailAddr string) (err error) {
- casefoldedAccount := client.Account()
- if casefoldedAccount == "" {
- return errAccountNotLoggedIn
- }
-
- if am.touchRegisterThrottle() {
- am.server.logger.Warning("accounts", "global registration throttle exceeded by client changing email", client.Nick())
- return errLimitExceeded
- }
-
- config := am.server.Config()
- if !config.Accounts.Registration.EmailVerification.Enabled {
- return errFeatureDisabled // redundant check, just in case
- }
- record := EmailChangeRecord{
- TimeCreated: time.Now().UTC(),
- Code: utils.GenerateSecretToken(),
- Email: emailAddr,
- }
- recordKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
- recordBytes, _ := json.Marshal(record)
- recordVal := string(recordBytes)
- am.server.store.Update(func(tx *buntdb.Tx) error {
- tx.Set(recordKey, recordVal, nil)
- return nil
- })
-
- if err != nil {
- return err
- }
-
- message := email.ComposeMail(config.Accounts.Registration.EmailVerification,
- emailAddr,
- fmt.Sprintf(client.t("Verify your change of e-mail address on %s"), am.server.name))
- message.WriteString(fmt.Sprintf(client.t("To confirm your change of e-mail address on %s, issue the following command:"), am.server.name))
- message.WriteString("\r\n")
- fmt.Fprintf(&message, "/MSG NickServ VERIFYEMAIL %s\r\n", record.Code)
-
- err = email.SendMail(config.Accounts.Registration.EmailVerification, emailAddr, message.Bytes())
- if err == nil {
- am.server.logger.Info("services",
- fmt.Sprintf("email change verification sent for account %s", casefoldedAccount))
- return
- } else {
- am.server.logger.Error("internal", "Failed to dispatch e-mail change verification to", emailAddr, err.Error())
- return ®istrationCallbackError{err}
- }
- }
-
- func (am *AccountManager) NsVerifyEmail(client *Client, code string) (err error) {
- casefoldedAccount := client.Account()
- if casefoldedAccount == "" {
- return errAccountNotLoggedIn
- }
-
- var record EmailChangeRecord
- success := false
- key := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
- ttl := time.Duration(am.server.Config().Accounts.Registration.VerifyTimeout)
- am.server.store.Update(func(tx *buntdb.Tx) error {
- rawStr, err := tx.Get(key)
- if err == nil && rawStr != "" {
- err := json.Unmarshal([]byte(rawStr), &record)
- if err == nil {
- if (ttl == 0 || time.Since(record.TimeCreated) < ttl) && utils.SecretTokensMatch(record.Code, code) {
- success = true
- tx.Delete(key)
- }
- }
- }
- return nil
- })
-
- if !success {
- return errAccountVerificationInvalidCode
- }
-
- munger := func(in AccountSettings) (out AccountSettings, err error) {
- out = in
- out.Email = record.Email
- return
- }
-
- _, err = am.ModifyAccountSettings(casefoldedAccount, munger)
- return
- }
-
- func (am *AccountManager) NsSendpass(client *Client, accountName string) (err error) {
- config := am.server.Config()
- if !(config.Accounts.Registration.EmailVerification.Enabled && config.Accounts.Registration.EmailVerification.PasswordReset.Enabled) {
- return errFeatureDisabled
- }
-
- account, err := am.LoadAccount(accountName)
- if err != nil {
- return err
- }
- if !account.Verified {
- return errAccountUnverified
- }
- if account.Suspended != nil {
- return errAccountSuspended
- }
- if account.Settings.Email == "" {
- return errValidEmailRequired
- }
-
- record := PasswordResetRecord{
- TimeCreated: time.Now().UTC(),
- Code: utils.GenerateSecretToken(),
- }
- recordKey := fmt.Sprintf(keyAccountPwReset, account.NameCasefolded)
- recordBytes, _ := json.Marshal(record)
- recordVal := string(recordBytes)
-
- am.server.store.Update(func(tx *buntdb.Tx) error {
- recStr, recErr := tx.Get(recordKey)
- if recErr == nil && recStr != "" {
- var existing PasswordResetRecord
- jErr := json.Unmarshal([]byte(recStr), &existing)
- cooldown := time.Duration(config.Accounts.Registration.EmailVerification.PasswordReset.Cooldown)
- if jErr == nil && time.Since(existing.TimeCreated) < cooldown {
- err = errLimitExceeded
- return nil
- }
- }
- tx.Set(recordKey, recordVal, &buntdb.SetOptions{
- Expires: true,
- TTL: time.Duration(config.Accounts.Registration.EmailVerification.PasswordReset.Timeout),
- })
- return nil
- })
-
- if err != nil {
- return
- }
-
- subject := fmt.Sprintf(client.t("Reset your password on %s"), am.server.name)
- message := email.ComposeMail(config.Accounts.Registration.EmailVerification, account.Settings.Email, subject)
- fmt.Fprintf(&message, client.t("We received a request to reset your password on %[1]s for account: %[2]s"), am.server.name, account.Name)
- message.WriteString("\r\n")
- fmt.Fprintf(&message, client.t("If you did not initiate this request, you can safely ignore this message."))
- message.WriteString("\r\n")
- message.WriteString("\r\n")
- message.WriteString(client.t("Otherwise, to reset your password, issue the following command (replace `new_password` with your desired password):"))
- message.WriteString("\r\n")
- fmt.Fprintf(&message, "/MSG NickServ RESETPASS %s %s new_password\r\n", account.Name, record.Code)
-
- err = email.SendMail(config.Accounts.Registration.EmailVerification, account.Settings.Email, message.Bytes())
- if err == nil {
- am.server.logger.Info("services",
- fmt.Sprintf("client %s sent a password reset email for account %s", client.Nick(), account.Name))
- } else {
- am.server.logger.Error("internal", "Failed to dispatch e-mail to", account.Settings.Email, err.Error())
- }
- return
-
- }
-
- func (am *AccountManager) NsResetpass(client *Client, accountName, code, password string) (err error) {
- if ValidatePassphrase(password) != nil {
- return errAccountBadPassphrase
- }
- account, err := am.LoadAccount(accountName)
- if err != nil {
- return
- }
- if !account.Verified {
- return errAccountUnverified
- }
- if account.Suspended != nil {
- return errAccountSuspended
- }
-
- success := false
- key := fmt.Sprintf(keyAccountPwReset, account.NameCasefolded)
- am.server.store.Update(func(tx *buntdb.Tx) error {
- rawStr, err := tx.Get(key)
- if err == nil && rawStr != "" {
- var record PasswordResetRecord
- err := json.Unmarshal([]byte(rawStr), &record)
- if err == nil && utils.SecretTokensMatch(record.Code, code) {
- success = true
- tx.Delete(key)
- }
- }
- return nil
- })
-
- if success {
- return am.setPassword(accountName, password, true)
- } else {
- return errAccountInvalidCredentials
- }
- }
-
- type PasswordResetRecord struct {
- TimeCreated time.Time
- Code string
- }
-
- 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
- }
- if err == nil && account.Credentials.SCRAMCreds.Iters == 0 {
- // XXX: if the account was created prior to 2.8, it doesn't have SCRAM credentials;
- // since we temporarily have access to a valid plaintext password, create them:
- am.rehashPassword(account.Name, passphrase)
- }
- 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
- am.rehashPassword(account, passphrase)
- return nil
- }
-
- func (am *AccountManager) rehashPassword(accountName, passphrase string) {
- err := am.setPassword(accountName, passphrase, true)
- if err != nil {
- am.server.logger.Error("internal", "could not upgrade user password", accountName, err.Error())
- }
- }
-
- 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)
- 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)
- 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.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)
- 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)
- pwResetKey := fmt.Sprintf(keyAccountPwReset, casefoldedAccount)
- emailChangeKey := fmt.Sprintf(keyAccountEmailChange, 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(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)
- tx.Delete(pwResetKey)
- tx.Delete(emailChangeKey)
-
- 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, *Session, []byte, *ResponseBuffer) bool{
- "PLAIN": authPlainHandler,
- "EXTERNAL": authExternalHandler,
- "SCRAM-SHA-256": authScramHandler,
- }
- )
-
- type CredentialsVersion int
-
- const (
- CredentialsLegacy CredentialsVersion = 0
- CredentialsSHA3Bcrypt CredentialsVersion = 1
- // negative numbers for migration
- CredentialsAtheme = -1
- CredentialsAnope = -2
- )
-
- type SCRAMCreds struct {
- Salt []byte
- Iters int
- StoredKey []byte
- ServerKey []byte
- }
-
- // AccountCredentials stores the various methods for verifying accounts.
- type AccountCredentials struct {
- Version CredentialsVersion
- PassphraseHash []byte
- Certfps []string
- SCRAMCreds
- }
-
- 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
- ac.SCRAMCreds = SCRAMCreds{}
- return nil
- }
-
- if ValidatePassphrase(passphrase) != nil {
- return errAccountBadPassphrase
- }
-
- ac.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), int(bcryptCost))
- if err != nil {
- return errAccountBadPassphrase
- }
-
- // we can pass an empty account name because it won't actually be incorporated
- // into the credentials; it's just a quirk of the xdg-go/scram API that the way
- // to produce server credentials is to call NewClient* and then GetStoredCredentials
- scramClient, err := scram.SHA256.NewClientUnprepped("", passphrase, "")
- if err != nil {
- return errAccountBadPassphrase
- }
- salt := make([]byte, 16)
- rand.Read(salt)
- // xdg-go/scram says: "Clients have a default minimum PBKDF2 iteration count of 4096."
- minIters := 4096
- scramCreds := scramClient.GetStoredCredentials(scram.KeyFactors{Salt: string(salt), Iters: minIters})
- ac.SCRAMCreds = SCRAMCreds{
- Salt: salt,
- Iters: minIters,
- StoredKey: scramCreds.StoredKey,
- ServerKey: scramCreds.ServerKey,
- }
-
- return nil
- }
-
- func (am *AccountManager) NewScramConversation() *scram.ServerConversation {
- server, _ := scram.SHA256.NewServer(am.lookupSCRAMCreds)
- return server.NewConversation()
- }
-
- func (am *AccountManager) lookupSCRAMCreds(accountName string) (creds scram.StoredCredentials, err error) {
- // strip client ID if present:
- if strudelIndex := strings.IndexByte(accountName, '@'); strudelIndex != -1 {
- accountName = accountName[:strudelIndex]
- }
-
- acct, err := am.LoadAccount(accountName)
- if err != nil {
- return
- }
- if acct.Credentials.SCRAMCreds.Iters == 0 {
- err = errNoSCRAMCredentials
- return
- }
- creds.Salt = string(acct.Credentials.SCRAMCreds.Salt)
- creds.Iters = acct.Credentials.SCRAMCreds.Iters
- creds.StoredKey = acct.Credentials.SCRAMCreds.StoredKey
- creds.ServerKey = acct.Credentials.SCRAMCreds.ServerKey
- return
- }
-
- 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
- )
-
- func replayJoinsSettingFromString(str string) (result ReplayJoinsSetting, err error) {
- switch strings.ToLower(str) {
- case "commands-only":
- result = ReplayJoinsCommandsOnly
- case "always":
- result = ReplayJoinsAlways
- 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
- Email string
- }
-
- // ClientAccount represents a user account.
- type ClientAccount struct {
- // Name of the account.
- Name string
- NameCasefolded string
- RegisteredAt time.Time
- 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
- Verified bool
- AdditionalNicks string
- VHost string
- Settings string
- Suspended string
- }
|