From ad73d68807b85ef834dbfea0517ad307d5d3f38d Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 11 Feb 2018 05:30:40 -0500 Subject: [PATCH] refactor account registration, add nick enforcement --- DEVELOPING.md | 2 +- irc/accountreg.go | 58 ----- irc/accounts.go | 517 ++++++++++++++++++++++++++++++++++----- irc/channel.go | 12 +- irc/chanserv.go | 4 +- irc/client.go | 30 +-- irc/client_lookup_set.go | 9 + irc/commands.go | 18 +- irc/config.go | 59 ++++- irc/errors.go | 31 ++- irc/getters.go | 28 ++- irc/handlers.go | 280 +++++++-------------- irc/idletimer.go | 77 ++++++ irc/nickname.go | 6 + irc/nickserv.go | 191 ++------------- irc/responsebuffer.go | 6 +- irc/server.go | 130 +++++----- oragono.yaml | 9 + 18 files changed, 865 insertions(+), 602 deletions(-) delete mode 100644 irc/accountreg.go diff --git a/DEVELOPING.md b/DEVELOPING.md index ced8680c..af5c83a5 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -98,7 +98,7 @@ In consequence, there is a lot of state (in particular, server and channel state There are some mutexes that are "tier 0": anything in a subpackage of `irc` (e.g., `irc/logger` or `irc/connection_limits`) shouldn't acquire mutexes defined in `irc`. -We are using `buntdb` for persistence; a `buntdb.DB` has an `RWMutex` inside it, with read-write transactions getting the `Lock()` and read-only transactions getting the `RLock()`. We haven't completely decided where this lock fits into the overall lock model. For now, it's probably better to err on the side of caution: if possible, don't acquire new locks inside the `buntdb` transaction, and be careful about what locks are held around the transaction as well. +We are using `buntdb` for persistence; a `buntdb.DB` has an `RWMutex` inside it, with read-write transactions getting the `Lock()` and read-only transactions getting the `RLock()`. This mutex is considered tier 1. However, it's shared globally across all consumers, so if possible you should avoid acquiring it while holding ordinary application-level mutexes. ## Command handlers and ResponseBuffer diff --git a/irc/accountreg.go b/irc/accountreg.go deleted file mode 100644 index 32ddda22..00000000 --- a/irc/accountreg.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2016-2017 Daniel Oaks -// released under the MIT license - -package irc - -import ( - "fmt" - - "github.com/tidwall/buntdb" -) - -// AccountRegistration manages the registration of accounts. -type AccountRegistration struct { - Enabled bool - EnabledCallbacks []string - EnabledCredentialTypes []string - AllowMultiplePerConnection bool -} - -// AccountCredentials stores the various methods for verifying accounts. -type AccountCredentials struct { - PassphraseSalt []byte - PassphraseHash []byte - Certificate string // fingerprint -} - -// NewAccountRegistration returns a new AccountRegistration, configured correctly. -func NewAccountRegistration(config AccountRegistrationConfig) (accountReg AccountRegistration) { - if config.Enabled { - accountReg.Enabled = true - accountReg.AllowMultiplePerConnection = config.AllowMultiplePerConnection - for _, name := range config.EnabledCallbacks { - // we store "none" as "*" internally - if name == "none" { - name = "*" - } - accountReg.EnabledCallbacks = append(accountReg.EnabledCallbacks, name) - } - // no need to make this configurable, right now at least - accountReg.EnabledCredentialTypes = []string{ - "passphrase", - "certfp", - } - } - return accountReg -} - -// removeFailedAccRegisterData removes the data created by ACC REGISTER if the account creation fails early. -func removeFailedAccRegisterData(store *buntdb.DB, account string) { - // error is ignored here, we can't do much about it anyways - store.Update(func(tx *buntdb.Tx) error { - tx.Delete(fmt.Sprintf(keyAccountExists, account)) - tx.Delete(fmt.Sprintf(keyAccountRegTime, account)) - tx.Delete(fmt.Sprintf(keyAccountCredentials, account)) - - return nil - }) -} diff --git a/irc/accounts.go b/irc/accounts.go index 6d8f3f77..8533cc3f 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -7,10 +7,13 @@ import ( "encoding/json" "fmt" "strconv" + "strings" + "sync" "time" "github.com/goshuirc/irc-go/ircfmt" "github.com/oragono/oragono/irc/caps" + "github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/sno" "github.com/tidwall/buntdb" ) @@ -24,6 +27,423 @@ const ( keyCertToAccount = "account.creds.certfp %s" ) +// 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 +} + +func NewAccountManager(server *Server) *AccountManager { + am := AccountManager{ + accountToClients: make(map[string][]*Client), + nickToAccount: make(map[string]string), + server: server, + } + + am.buildNickToAccountIndex() + return &am +} + +func (am *AccountManager) buildNickToAccountIndex() { + if am.server.AccountConfig().NickReservation == NickReservationDisabled { + return + } + + result := make(map[string]string) + 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 + } + accountName := strings.TrimPrefix(key, existsPrefix) + if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil { + result[accountName] = accountName + } + return true + }) + return err + }) + + if err != nil { + am.server.logger.Error("internal", fmt.Sprintf("couldn't read reserved nicks: %v", err)) + } else { + am.Lock() + am.nickToAccount = result + am.Unlock() + } + + return +} + +func (am *AccountManager) NickToAccount(cfnick string) string { + am.RLock() + defer am.RUnlock() + return am.nickToAccount[cfnick] +} + +func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error { + casefoldedAccount, err := CasefoldName(account) + if err != nil || account == "" || account == "*" { + return errAccountCreation + } + + accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount) + accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount) + registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) + credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) + certFPKey := fmt.Sprintf(keyCertToAccount, certfp) + + var creds AccountCredentials + // always set passphrase salt + creds.PassphraseSalt, err = passwd.NewSalt() + if err != nil { + return errAccountCreation + } + // it's fine if this is empty, that just means no certificate is authorized + creds.Certificate = certfp + if passphrase != "" { + creds.PassphraseHash, err = am.server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase) + if err != nil { + am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err)) + return errAccountCreation + } + } + + credText, err := json.Marshal(creds) + if err != nil { + am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err)) + return errAccountCreation + } + credStr := string(credText) + + registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10) + + var setOptions *buntdb.SetOptions + ttl := am.server.AccountConfig().Registration.VerifyTimeout + if ttl != 0 { + setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl} + } + + err = am.server.store.Update(func(tx *buntdb.Tx) error { + _, 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) + if certfp != "" { + tx.Set(certFPKey, casefoldedAccount, setOptions) + } + return nil + }) + + if err != nil { + return err + } + + return nil +} + +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) + credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) + + var raw rawClientAccount + + func() { + am.serialCacheUpdateMutex.Lock() + defer am.serialCacheUpdateMutex.Unlock() + + 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 + } + + // TODO add code verification here + // return errAccountVerificationFailed if it fails + + // verify the account + tx.Set(verifiedKey, "1", nil) + // 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) + + 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) + if creds.Certificate != "" { + certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate) + tx.Set(certFPKey, casefoldedAccount, nil) + } + + return nil + }) + + if err == nil { + am.Lock() + am.nickToAccount[casefoldedAccount] = casefoldedAccount + am.Unlock() + } + }() + + if err != nil { + return err + } + + am.Login(client, raw.Name) + return nil +} + +func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error { + casefoldedAccount, err := CasefoldName(accountName) + if err != nil { + return errAccountDoesNotExist + } + + account, err := am.LoadAccount(casefoldedAccount) + if err != nil { + return err + } + + if !account.Verified { + return errAccountUnverified + } + + err = am.server.passwords.CompareHashAndPassword( + account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase) + if err != nil { + return errAccountInvalidCredentials + } + + am.Login(client, account.Name) + return nil +} + +func (am *AccountManager) LoadAccount(casefoldedAccount string) (result ClientAccount, err error) { + var raw rawClientAccount + am.server.store.View(func(tx *buntdb.Tx) error { + raw, err = am.loadRawAccount(tx, casefoldedAccount) + if err == buntdb.ErrNotFound { + err = errAccountDoesNotExist + } + return nil + }) + if err != nil { + return + } + + result.Name = raw.Name + regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64) + result.RegisteredAt = time.Unix(regTimeInt, 0) + e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials) + if e != nil { + am.server.logger.Error("internal", fmt.Sprintf("could not unmarshal credentials: %v", e)) + err = errAccountDoesNotExist + return + } + result.Verified = raw.Verified + 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) + + _, e := tx.Get(accountKey) + if e == buntdb.ErrNotFound { + err = errAccountDoesNotExist + return + } + + if result.Name, err = tx.Get(accountNameKey); err != nil { + return + } + if result.RegisteredAt, err = tx.Get(registeredTimeKey); err != nil { + return + } + if result.Credentials, err = tx.Get(credentialsKey); err != nil { + return + } + if _, e = tx.Get(verifiedKey); e == nil { + result.Verified = true + } + + return +} + +func (am *AccountManager) Unregister(account string) error { + 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) + verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) + + var clients []*Client + + func() { + var credText string + + am.serialCacheUpdateMutex.Lock() + defer am.serialCacheUpdateMutex.Unlock() + + am.server.store.Update(func(tx *buntdb.Tx) error { + tx.Delete(accountKey) + tx.Delete(accountNameKey) + tx.Delete(verifiedKey) + tx.Delete(registeredTimeKey) + credText, err = tx.Get(credentialsKey) + tx.Delete(credentialsKey) + return nil + }) + + if err == nil { + var creds AccountCredentials + if err = json.Unmarshal([]byte(credText), &creds); err == nil && creds.Certificate != "" { + certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate) + am.server.store.Update(func(tx *buntdb.Tx) error { + if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount { + tx.Delete(certFPKey) + } + return nil + }) + } + } + + am.Lock() + defer am.Unlock() + clients = am.accountToClients[casefoldedAccount] + delete(am.accountToClients, casefoldedAccount) + // TODO when registration of multiple nicks is fully implemented, + // save the nicks that were deleted from the store and delete them here: + delete(am.nickToAccount, casefoldedAccount) + }() + + for _, client := range clients { + client.LogoutOfAccount() + } + + return nil +} + +func (am *AccountManager) AuthenticateByCertFP(client *Client) error { + if client.certfp == "" { + return errAccountInvalidCredentials + } + + var account string + var rawAccount rawClientAccount + certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp) + + err := am.server.store.Update(func(tx *buntdb.Tx) error { + var err error + account, _ = tx.Get(certFPKey) + if account == "" { + return errAccountInvalidCredentials + } + rawAccount, err = am.loadRawAccount(tx, account) + if err != nil || !rawAccount.Verified { + return errAccountUnverified + } + return nil + }) + + if err != nil { + return err + } + + // ok, we found an account corresponding to their certificate + + am.Login(client, rawAccount.Name) + return nil +} + +func (am *AccountManager) Login(client *Client, account string) { + client.LoginToAccount(account) + + casefoldedAccount, _ := CasefoldName(account) + am.Lock() + defer am.Unlock() + am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client) +} + +func (am *AccountManager) Logout(client *Client) { + casefoldedAccount := client.Account() + if casefoldedAccount == "" || casefoldedAccount == "*" { + return + } + + client.LogoutOfAccount() + + am.Lock() + defer am.Unlock() + + if client.LoggedIntoAccount() { + return + } + + 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 + return +} + 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. @@ -31,95 +451,62 @@ var ( "PLAIN": authPlainHandler, "EXTERNAL": authExternalHandler, } - - // NoAccount is a placeholder which means that the user is not logged into an account. - NoAccount = ClientAccount{ - Name: "*", // * is used until actual account name is set - } ) +// AccountCredentials stores the various methods for verifying accounts. +type AccountCredentials struct { + PassphraseSalt []byte + PassphraseHash []byte + Certificate string // fingerprint +} + // ClientAccount represents a user account. type ClientAccount struct { // Name of the account. Name string // RegisteredAt represents the time that the account was registered. RegisteredAt time.Time - // Clients that are currently logged into this account (useful for notifications). - Clients []*Client + Credentials AccountCredentials + Verified bool } -// loadAccountCredentials loads an account's credentials from the store. -func loadAccountCredentials(tx *buntdb.Tx, accountKey string) (*AccountCredentials, error) { - credText, err := tx.Get(fmt.Sprintf(keyAccountCredentials, accountKey)) - if err != nil { - return nil, err - } - - var creds AccountCredentials - err = json.Unmarshal([]byte(credText), &creds) - if err != nil { - return nil, err - } - - return &creds, nil -} - -// loadAccount loads an account from the store, note that the account must actually exist. -func loadAccount(server *Server, tx *buntdb.Tx, accountKey string) *ClientAccount { - name, _ := tx.Get(fmt.Sprintf(keyAccountName, accountKey)) - regTime, _ := tx.Get(fmt.Sprintf(keyAccountRegTime, accountKey)) - regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) - accountInfo := ClientAccount{ - Name: name, - RegisteredAt: time.Unix(regTimeInt, 0), - Clients: []*Client{}, - } - server.accounts[accountKey] = &accountInfo - - return &accountInfo +// convenience for passing around raw serialized account data +type rawClientAccount struct { + Name string + RegisteredAt string + Credentials string + Verified bool } // LoginToAccount logs the client into the given account. -func (client *Client) LoginToAccount(account *ClientAccount) { - if client.account == account { - // already logged into this acct, no changing necessary +func (client *Client) LoginToAccount(account string) { + casefoldedAccount, err := CasefoldName(account) + if err != nil { return - } else if client.LoggedIntoAccount() { - // logout of existing acct - var newClientAccounts []*Client - for _, c := range account.Clients { - if c != client { - newClientAccounts = append(newClientAccounts, c) - } - } - account.Clients = newClientAccounts } - account.Clients = append(account.Clients, client) - client.account = account - client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]"), client.nickMaskString, account.Name)) + if client.Account() == casefoldedAccount { + // already logged into this acct, no changing necessary + return + } + + client.SetAccountName(casefoldedAccount) + client.nickTimer.Touch() + + client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]"), client.nickMaskString, casefoldedAccount)) //TODO(dan): This should output the AccountNotify message instead of the sasl accepted function below. } // LogoutOfAccount logs the client out of their current account. func (client *Client) LogoutOfAccount() { - account := client.account - if account == nil { + if client.Account() == "" { // already logged out return } - // logout of existing acct - var newClientAccounts []*Client - for _, c := range account.Clients { - if c != client { - newClientAccounts = append(newClientAccounts, c) - } - } - account.Clients = newClientAccounts - - client.account = nil + client.SetAccountName("") + client.nickTimer.Touch() // dispatch account-notify for friend := range client.Friends(caps.AccountNotify) { @@ -129,11 +516,11 @@ func (client *Client) LogoutOfAccount() { // successfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages. func (client *Client) successfulSaslAuth(rb *ResponseBuffer) { - rb.Add(nil, client.server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, client.account.Name, fmt.Sprintf("You are now logged in as %s", client.account.Name)) + rb.Add(nil, client.server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, client.AccountName(), fmt.Sprintf("You are now logged in as %s", client.AccountName())) rb.Add(nil, client.server.name, RPL_SASLSUCCESS, client.nick, client.t("SASL authentication successful")) // dispatch account-notify for friend := range client.Friends(caps.AccountNotify) { - friend.Send(nil, client.nickMaskString, "ACCOUNT", client.account.Name) + friend.Send(nil, client.nickMaskString, "ACCOUNT", client.AccountName()) } } diff --git a/irc/channel.go b/irc/channel.go index e9ae228e..12ad927f 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -383,13 +383,13 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) { for _, member := range channel.Members() { if member == client { if member.capabilities.Has(caps.ExtendedJoin) { - rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname) + rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname) } else { rb.Add(nil, client.nickMaskString, "JOIN", channel.name) } } else { if member.capabilities.Has(caps.ExtendedJoin) { - member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname) + member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname) } else { member.Send(nil, client.nickMaskString, "JOIN", channel.name) } @@ -407,7 +407,9 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) { // give channel mode if necessary newChannel := firstJoin && !channel.IsRegistered() var givenMode *modes.Mode - if client.AccountName() == channel.registeredFounder { + account := client.Account() + cffounder, _ := CasefoldName(channel.registeredFounder) + if account != "" && account == cffounder { givenMode = &modes.ChannelFounder } else if newChannel { givenMode = &modes.ChannelOperator @@ -419,7 +421,7 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) { } if client.capabilities.Has(caps.ExtendedJoin) { - rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname) + rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname) } else { rb.Add(nil, client.nickMaskString, "JOIN", channel.name) } @@ -526,7 +528,7 @@ func (channel *Channel) CanSpeak(client *Client) bool { if channel.flags[modes.Moderated] && !channel.ClientIsAtLeast(client, modes.Voice) { return false } - if channel.flags[modes.RegisteredOnly] && client.account == &NoAccount { + if channel.flags[modes.RegisteredOnly] && client.Account() == "" { return false } return true diff --git a/irc/chanserv.go b/irc/chanserv.go index 6ddb4763..3714b706 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -70,13 +70,13 @@ func (server *Server) chanservRegisterHandler(client *Client, channelName string return } - if client.account == &NoAccount { + if client.Account() == "" { rb.ChanServNotice(client.t("You must be logged in to register a channel")) return } // this provides the synchronization that allows exactly one registration of the channel: - err = channelInfo.SetRegistered(client.AccountName()) + err = channelInfo.SetRegistered(client.Account()) if err != nil { rb.ChanServNotice(err.Error()) return diff --git a/irc/client.go b/irc/client.go index 997c9183..8fa15dac 100644 --- a/irc/client.go +++ b/irc/client.go @@ -36,7 +36,8 @@ var ( // Client is an IRC client. type Client struct { - account *ClientAccount + account string + accountName string atime time.Time authorized bool awayMessage string @@ -62,6 +63,7 @@ type Client struct { nickCasefolded string nickMaskCasefolded string nickMaskString string // cache for nickmask string since it's used with lots of replies + nickTimer *NickTimer operName string proxiedIP net.IP // actual remote IP if using the PROXY protocol quitMessage string @@ -96,7 +98,6 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client { flags: make(map[modes.Mode]bool), server: server, socket: &socket, - account: &NoAccount, nick: "*", // * is used until actual nick is given nickCasefolded: "*", nickMaskString: "*", // * is used until actual nick is given @@ -217,6 +218,8 @@ func (client *Client) run() { client.idletimer = NewIdleTimer(client) client.idletimer.Start() + client.nickTimer = NewNickTimer(client) + // Set the hostname for this client // (may be overridden by a later PROXY command from stunnel) client.rawHostname = utils.AddrLookupHostname(client.socket.conn.RemoteAddr()) @@ -299,7 +302,6 @@ func (client *Client) Register() { client.TryResume() // finish registration - client.Touch() client.updateNickMask("") client.server.monitorManager.AlertAbout(client, true) } @@ -338,8 +340,8 @@ func (client *Client) TryResume() { return } - oldAccountName := oldClient.AccountName() - newAccountName := client.AccountName() + oldAccountName := oldClient.Account() + newAccountName := client.Account() if oldAccountName == "" || newAccountName == "" || oldAccountName != newAccountName { client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, old and new clients must be logged into the same account")) @@ -406,7 +408,7 @@ func (client *Client) TryResume() { } if member.capabilities.Has(caps.ExtendedJoin) { - member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname) + member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname) } else { member.Send(nil, client.nickMaskString, "JOIN", channel.name) } @@ -589,7 +591,7 @@ func (client *Client) AllNickmasks() []string { // LoggedIntoAccount returns true if this client is logged into an account. func (client *Client) LoggedIntoAccount() bool { - return client.account != nil && client.account != &NoAccount + return client.Account() != "" } // RplISupport outputs our ISUPPORT lines to the client. This is used on connection and in VERSION responses. @@ -687,6 +689,8 @@ func (client *Client) destroy(beingResumed bool) { client.idletimer.Stop() } + client.server.accounts.Logout(client) + client.socket.Close() // send quit messages to friends @@ -723,11 +727,11 @@ func (client *Client) SendSplitMsgFromClient(msgid string, from *Client, tags *m // Adds account-tag to the line as well. func (client *Client) SendFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) error { // attach account-tag - if client.capabilities.Has(caps.AccountTag) && from.account != &NoAccount { + if client.capabilities.Has(caps.AccountTag) && client.LoggedIntoAccount() { if tags == nil { - tags = ircmsg.MakeTags("account", from.account.Name) + tags = ircmsg.MakeTags("account", from.AccountName()) } else { - (*tags)["account"] = ircmsg.MakeTagValue(from.account.Name) + (*tags)["account"] = ircmsg.MakeTagValue(from.AccountName()) } } // attach message-id @@ -772,10 +776,8 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage) error { maxlenTags, maxlenRest := client.maxlens() line, err := message.LineMaxLen(maxlenTags, maxlenRest) if err != nil { - // try not to fail quietly - especially useful when running tests, as a note to dig deeper - // log.Println("Error assembling message:") - // spew.Dump(message) - // debug.PrintStack() + logline := fmt.Sprintf("Error assembling message for sending: %v\n%s", err, debug.Stack()) + client.server.logger.Error("internal", logline) message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending") line, _ := message.Line() diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 94e53cc4..005847f6 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -98,6 +98,12 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error { return err } + var reservedAccount string + reservation := client.server.AccountConfig().NickReservation + if reservation != NickReservationDisabled { + reservedAccount = client.server.accounts.NickToAccount(newcfnick) + } + clients.Lock() defer clients.Unlock() @@ -107,6 +113,9 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error { if currentNewEntry != nil && currentNewEntry != client { return errNicknameInUse } + if reservation == NickReservationStrict && reservedAccount != client.Account() { + return errNicknameReserved + } clients.byNick[newcfnick] = client client.updateNickMask(newNick) return nil diff --git a/irc/commands.go b/irc/commands.go index 1ee0c35f..ff0cf5ae 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -39,12 +39,7 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters")) return false } - if !cmd.leaveClientActive { - client.Active() - } - if !cmd.leaveClientIdle { - client.Touch() - } + rb := NewResponseBuffer(client) rb.Label = GetLabel(msg) @@ -57,6 +52,14 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b server.tryRegister(client) } + if !cmd.leaveClientIdle { + client.Touch() + } + + if !cmd.leaveClientActive { + client.Active() + } + return exiting } @@ -67,7 +70,7 @@ func init() { Commands = map[string]Command{ "ACC": { handler: accHandler, - minParams: 3, + minParams: 2, }, "AMBIANCE": { handler: sceneHandler, @@ -98,6 +101,7 @@ func init() { "DEBUG": { handler: debugHandler, minParams: 1, + oper: true, }, "DLINE": { handler: dlineHandler, diff --git a/irc/config.go b/irc/config.go index 3a7ecffb..06f649ad 100644 --- a/irc/config.go +++ b/irc/config.go @@ -8,6 +8,7 @@ package irc import ( "crypto/tls" "encoding/json" + "errors" "fmt" "io/ioutil" "log" @@ -57,11 +58,49 @@ func (conf *PassConfig) PasswordBytes() []byte { return bytes } +type NickReservation int + +const ( + NickReservationDisabled NickReservation = iota + NickReservationWithTimeout + NickReservationStrict +) + +func (nr *NickReservation) UnmarshalYAML(unmarshal func(interface{}) error) error { + var orig, raw string + var err error + if err = unmarshal(&orig); err != nil { + return err + } + if raw, err = Casefold(orig); err != nil { + return err + } + if raw == "disabled" || raw == "false" || raw == "" { + *nr = NickReservationDisabled + } else if raw == "timeout" { + *nr = NickReservationWithTimeout + } else if raw == "strict" { + *nr = NickReservationStrict + } else { + return errors.New(fmt.Sprintf("invalid nick-reservation value: %s", orig)) + } + return nil +} + +type AccountConfig struct { + Registration AccountRegistrationConfig + AuthenticationEnabled bool `yaml:"authentication-enabled"` + NickReservation NickReservation `yaml:"nick-reservation"` + NickReservationTimeout time.Duration `yaml:"nick-reservation-timeout"` +} + // AccountRegistrationConfig controls account registration. type AccountRegistrationConfig struct { - Enabled bool - EnabledCallbacks []string `yaml:"enabled-callbacks"` - Callbacks struct { + Enabled bool + EnabledCallbacks []string `yaml:"enabled-callbacks"` + EnabledCredentialTypes []string `yaml:"-"` + VerifyTimeout time.Duration `yaml:"verify-timeout"` + Callbacks struct { Mailto struct { Server string Port int @@ -180,10 +219,7 @@ type Config struct { Path string } - Accounts struct { - Registration AccountRegistrationConfig - AuthenticationEnabled bool `yaml:"authentication-enabled"` - } + Accounts AccountConfig Channels struct { DefaultModes *string `yaml:"default-modes"` @@ -469,6 +505,15 @@ func LoadConfig(filename string) (config *Config, err error) { } config.Logging = newLogConfigs + // hardcode this for now + config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"} + for i, name := range config.Accounts.Registration.EnabledCallbacks { + if name == "none" { + // we store "none" as "*" internally + config.Accounts.Registration.EnabledCallbacks[i] = "*" + } + } + config.Server.MaxSendQBytes, err = bytefmt.ToBytes(config.Server.MaxSendQString) if err != nil { return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error()) diff --git a/irc/errors.go b/irc/errors.go index 9fc0d97b..d29bcc3a 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -9,18 +9,25 @@ import "errors" // Runtime Errors var ( - errAccountCreation = errors.New("Account could not be created") - errCertfpAlreadyExists = errors.New("An account already exists with your certificate") - errChannelAlreadyRegistered = errors.New("Channel is already registered") - errChannelNameInUse = errors.New("Channel name in use") - errInvalidChannelName = errors.New("Invalid channel name") - errMonitorLimitExceeded = errors.New("Monitor limit exceeded") - errNickMissing = errors.New("nick missing") - errNicknameInUse = errors.New("nickname in use") - errNoExistingBan = errors.New("Ban does not exist") - errNoSuchChannel = errors.New("No such channel") - errRenamePrivsNeeded = errors.New("Only chanops can rename channels") - errSaslFail = errors.New("SASL failed") + errAccountAlreadyRegistered = errors.New("Account already exists") + errAccountCreation = errors.New("Account could not be created") + errAccountDoesNotExist = errors.New("Account does not exist") + errAccountVerificationFailed = errors.New("Account verification failed") + errAccountUnverified = errors.New("Account is not yet verified") + errAccountAlreadyVerified = errors.New("Account is already verified") + errAccountInvalidCredentials = errors.New("Invalid account credentials") + errCertfpAlreadyExists = errors.New("An account already exists with your certificate") + errChannelAlreadyRegistered = errors.New("Channel is already registered") + errChannelNameInUse = errors.New("Channel name in use") + errInvalidChannelName = errors.New("Invalid channel name") + errMonitorLimitExceeded = errors.New("Monitor limit exceeded") + errNickMissing = errors.New("nick missing") + errNicknameInUse = errors.New("nickname in use") + errNicknameReserved = errors.New("nickname is reserved") + errNoExistingBan = errors.New("Ban does not exist") + errNoSuchChannel = errors.New("No such channel") + errRenamePrivsNeeded = errors.New("Only chanops can rename channels") + errSaslFail = errors.New("SASL failed") ) // Socket Errors diff --git a/irc/getters.go b/irc/getters.go index a00f0054..ab5de049 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -56,6 +56,12 @@ func (server *Server) ChannelRegistrationEnabled() bool { return server.channelRegistrationEnabled } +func (server *Server) AccountConfig() *AccountConfig { + server.configurableStateMutex.RLock() + defer server.configurableStateMutex.RUnlock() + return server.accountConfig +} + func (client *Client) Nick() string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() @@ -104,10 +110,30 @@ func (client *Client) Destroyed() bool { return client.isDestroyed } +func (client *Client) Account() string { + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + return client.account +} + func (client *Client) AccountName() string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() - return client.account.Name + if client.accountName == "" { + return "*" + } + return client.accountName +} + +func (client *Client) SetAccountName(account string) { + var casefoldedAccount string + if account != "" { + casefoldedAccount, _ = CasefoldName(account) + } + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + client.account = casefoldedAccount + client.accountName = account } func (client *Client) HasMode(mode modes.Mode) bool { diff --git a/irc/handlers.go b/irc/handlers.go index 10d4e332..23730178 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -11,7 +11,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "log" "net" "os" "runtime" @@ -42,6 +41,8 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo return accRegisterHandler(server, client, msg, rb) } else if subcommand == "verify" { rb.Notice(client.t("VERIFY is not yet implemented")) + } else if subcommand == "unregister" { + return accUnregisterHandler(server, client, msg, rb) } else { rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand")) } @@ -49,18 +50,45 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo return false } +// ACC UNREGISTER +func accUnregisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + // get and sanitise account name + account := strings.TrimSpace(msg.Params[1]) + casefoldedAccount, err := CasefoldName(account) + // probably don't need explicit check for "*" here... but let's do it anyway just to make sure + if err != nil || msg.Params[1] == "*" { + rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, account, client.t("Account name is not valid")) + return false + } + + if !(account == client.Account() || client.HasRoleCapabs("unregister")) { + rb.Add(nil, server.name, ERR_NOPRIVS, client.Nick(), account, client.t("Insufficient oper privs")) + return false + } + + err = server.accounts.Unregister(account) + // TODO better responses all around here + if err != nil { + errorMsg := fmt.Sprintf("Unknown error while unregistering account %s", casefoldedAccount) + rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), msg.Command, errorMsg) + return false + } + rb.Notice(fmt.Sprintf("Successfully unregistered account %s", casefoldedAccount)) + return false +} + // ACC REGISTER [callback_namespace:] [cred_type] : func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { // make sure reg is enabled - if !server.accountRegistration.Enabled { + if !server.AccountConfig().Registration.Enabled { rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled")) return false } // clients can't reg new accounts if they're already logged in if client.LoggedIntoAccount() { - if server.accountRegistration.AllowMultiplePerConnection { - client.LogoutOfAccount() + if server.AccountConfig().Registration.AllowMultiplePerConnection { + server.accounts.Logout(client) } else { rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("You're already logged into an account")) return false @@ -76,36 +104,11 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r return false } - // check whether account exists - // do it all in one write tx to prevent races - err = server.store.Update(func(tx *buntdb.Tx) error { - accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount) - - _, err := tx.Get(accountKey) - if err != buntdb.ErrNotFound { - //TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be - rb.Add(nil, server.name, ERR_ACCOUNT_ALREADY_EXISTS, client.nick, account, client.t("Account already exists")) - return errAccountCreation - } - - registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) - - tx.Set(accountKey, "1", nil) - tx.Set(fmt.Sprintf(keyAccountName, casefoldedAccount), account, nil) - tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil) - return nil - }) - - // account could not be created and relevant numerics have been dispatched, abort - if err != nil { - if err != errAccountCreation { - rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", client.t("Could not register")) - log.Println("Could not save registration initial data:", err.Error()) - } + if len(msg.Params) < 4 { + rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters")) return false } - // account didn't already exist, continue with account creation and dispatching verification (if required) callback := strings.ToLower(msg.Params[2]) var callbackNamespace, callbackValue string @@ -115,14 +118,14 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r callbackValues := strings.SplitN(callback, ":", 2) callbackNamespace, callbackValue = callbackValues[0], callbackValues[1] } else { - callbackNamespace = server.accountRegistration.EnabledCallbacks[0] + callbackNamespace = server.AccountConfig().Registration.EnabledCallbacks[0] callbackValue = callback } // ensure the callback namespace is valid // need to search callback list, maybe look at using a map later? var callbackValid bool - for _, name := range server.accountRegistration.EnabledCallbacks { + for _, name := range server.AccountConfig().Registration.EnabledCallbacks { if callbackNamespace == name { callbackValid = true } @@ -130,7 +133,6 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r if !callbackValid { rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackNamespace, client.t("Callback namespace is not supported")) - removeFailedAccRegisterData(server.store, casefoldedAccount) return false } @@ -140,116 +142,62 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r if len(msg.Params) > 4 { credentialType = strings.ToLower(msg.Params[3]) credentialValue = msg.Params[4] - } else if len(msg.Params) == 4 { + } else { + // exactly 4 params credentialType = "passphrase" // default from the spec credentialValue = msg.Params[3] - } else { - rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters")) - removeFailedAccRegisterData(server.store, casefoldedAccount) - return false } // ensure the credential type is valid var credentialValid bool - for _, name := range server.accountRegistration.EnabledCredentialTypes { + for _, name := range server.AccountConfig().Registration.EnabledCredentialTypes { if credentialType == name { credentialValid = true } } if credentialType == "certfp" && client.certfp == "" { rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate")) - removeFailedAccRegisterData(server.store, casefoldedAccount) return false } if !credentialValid { rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("Credential type is not supported")) - removeFailedAccRegisterData(server.store, casefoldedAccount) return false } - // store details - err = server.store.Update(func(tx *buntdb.Tx) error { - // certfp special lookup key - if credentialType == "certfp" { - assembledKeyCertToAccount := fmt.Sprintf(keyCertToAccount, client.certfp) - - // make sure certfp doesn't already exist because that'd be silly - _, err := tx.Get(assembledKeyCertToAccount) - if err != buntdb.ErrNotFound { - return errCertfpAlreadyExists - } - - tx.Set(assembledKeyCertToAccount, casefoldedAccount, nil) - } - - // make creds - var creds AccountCredentials - - // always set passphrase salt - creds.PassphraseSalt, err = passwd.NewSalt() - if err != nil { - return fmt.Errorf("Could not create passphrase salt: %s", err.Error()) - } - - if credentialType == "certfp" { - creds.Certificate = client.certfp - } else if credentialType == "passphrase" { - creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, credentialValue) - if err != nil { - return fmt.Errorf("Could not hash password: %s", err) - } - } - credText, err := json.Marshal(creds) - if err != nil { - return fmt.Errorf("Could not marshal creds: %s", err) - } - tx.Set(fmt.Sprintf(keyAccountCredentials, account), string(credText), nil) - - return nil - }) - - // details could not be stored and relevant numerics have been dispatched, abort + var passphrase, certfp string + if credentialType == "certfp" { + certfp = client.certfp + } else if credentialType == "passphrase" { + passphrase = credentialValue + } + err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp) if err != nil { - errMsg := "Could not register" + msg := "Unknown" + code := ERR_UNKNOWNERROR if err == errCertfpAlreadyExists { - errMsg = "An account already exists for your certificate fingerprint" + msg = "An account already exists for your certificate fingerprint" + } else if err == errAccountAlreadyRegistered { + msg = "Account already exists" + code = ERR_ACCOUNT_ALREADY_EXISTS } - rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", errMsg) - log.Println("Could not save registration creds:", err.Error()) - removeFailedAccRegisterData(server.store, casefoldedAccount) + if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists { + msg = err.Error() + } + rb.Add(nil, server.name, code, client.nick, "ACC", "REGISTER", client.t(msg)) return false } // automatically complete registration if callbackNamespace == "*" { - err = server.store.Update(func(tx *buntdb.Tx) error { - tx.Set(fmt.Sprintf(keyAccountVerified, casefoldedAccount), "1", nil) - - // load acct info inside store tx - account := ClientAccount{ - Name: strings.TrimSpace(msg.Params[1]), - RegisteredAt: time.Now(), - Clients: []*Client{client}, - } - //TODO(dan): Consider creating ircd-wide account adding/removing/affecting lock for protecting access to these sorts of variables - server.accounts[casefoldedAccount] = &account - client.account = &account - - rb.Add(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, account.Name, client.t("Account created")) - rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account.Name, fmt.Sprintf(client.t("You are now logged in as %s"), account.Name)) - rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful")) - server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), account.Name, client.nickMaskString)) - return nil - }) + err := server.accounts.Verify(client, casefoldedAccount, "") if err != nil { - rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", client.t("Could not register")) - log.Println("Could not save verification confirmation (*):", err.Error()) - removeFailedAccRegisterData(server.store, casefoldedAccount) return false } - - return false + client.Send(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, casefoldedAccount, client.t("Account created")) + client.Send(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount)) + client.Send(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful")) + server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), casefoldedAccount, client.nickMaskString)) } // dispatch callback @@ -261,7 +209,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r // AUTHENTICATE [||*] func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { // sasl abort - if !server.accountAuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" { + if !server.AccountConfig().AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" { rb.Add(nil, server.name, ERR_SASLABORTED, client.nick, client.t("SASL authentication aborted")) client.saslInProgress = false client.saslMechanism = "" @@ -374,40 +322,11 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value [] return false } - // load and check acct data all in one update to prevent races. - // as noted elsewhere, change to proper locking for Account type later probably - err = server.store.Update(func(tx *buntdb.Tx) error { - // confirm account is verified - _, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey)) - if err != nil { - return errSaslFail - } - - creds, err := loadAccountCredentials(tx, accountKey) - if err != nil { - return err - } - - // ensure creds are valid - password := string(splitValue[2]) - if len(creds.PassphraseHash) < 1 || len(creds.PassphraseSalt) < 1 || len(password) < 1 { - return errSaslFail - } - err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, password) - - // succeeded, load account info if necessary - account, exists := server.accounts[accountKey] - if !exists { - account = loadAccount(server, tx, accountKey) - } - - client.LoginToAccount(account) - - return err - }) - + password := string(splitValue[2]) + err = server.accounts.AuthenticateByPassphrase(client, accountKey, password) if err != nil { - rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed")) + msg := authErrorToMessage(server, err) + rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg))) return false } @@ -415,6 +334,16 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value [] return false } +func authErrorToMessage(server *Server, err error) (msg string) { + if err == errAccountDoesNotExist || err == errAccountUnverified || err == errAccountInvalidCredentials { + msg = err.Error() + } else { + server.logger.Error("internal", fmt.Sprintf("sasl authentication failure: %v", err)) + msg = "Unknown" + } + return +} + // AUTHENTICATE EXTERNAL func authExternalHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool { if client.certfp == "" { @@ -422,44 +351,10 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value return false } - err := server.store.Update(func(tx *buntdb.Tx) error { - // certfp lookup key - accountKey, err := tx.Get(fmt.Sprintf(keyCertToAccount, client.certfp)) - if err != nil { - return errSaslFail - } - - // confirm account exists - _, err = tx.Get(fmt.Sprintf(keyAccountExists, accountKey)) - if err != nil { - return errSaslFail - } - - // confirm account is verified - _, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey)) - if err != nil { - return errSaslFail - } - - // confirm the certfp in that account's credentials - creds, err := loadAccountCredentials(tx, accountKey) - if err != nil || creds.Certificate != client.certfp { - return errSaslFail - } - - // succeeded, load account info if necessary - account, exists := server.accounts[accountKey] - if !exists { - account = loadAccount(server, tx, accountKey) - } - - client.LoginToAccount(account) - - return nil - }) - + err := server.accounts.AuthenticateByCertFP(client) if err != nil { - rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed")) + msg := authErrorToMessage(server, err) + rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg))) return false } @@ -582,11 +477,16 @@ func csHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respon // DEBUG func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - if !client.flags[modes.Operator] { + param, err := Casefold(msg.Params[0]) + if err != nil { return false } - switch msg.Params[0] { + if !client.HasMode(modes.Operator) { + return false + } + + switch param { case "GCSTATS": stats := debug.GCStats{ Pause: make([]time.Duration, 10), @@ -2107,7 +2007,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re } founder := channel.Founder() - if founder != "" && founder != client.AccountName() { + if founder != "" && founder != client.Account() { //TODO(dan): Change this to ERR_CANNOTRENAME rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "RENAME", oldName, client.t("Only channel founders can change registered channels")) return false @@ -2130,11 +2030,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re } else { mcl.Send(nil, mcl.nickMaskString, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason)) if mcl.capabilities.Has(caps.ExtendedJoin) { - accountName := "*" - if mcl.account != nil { - accountName = mcl.account.Name - } - mcl.Send(nil, mcl.nickMaskString, "JOIN", newName, accountName, mcl.realname) + mcl.Send(nil, mcl.nickMaskString, "JOIN", newName, mcl.AccountName(), mcl.realname) } else { mcl.Send(nil, mcl.nickMaskString, "JOIN", newName) } diff --git a/irc/idletimer.go b/irc/idletimer.go index d24f694b..7d5dcd9b 100644 --- a/irc/idletimer.go +++ b/irc/idletimer.go @@ -165,3 +165,80 @@ func (it *IdleTimer) quitMessage(state TimerState) string { return "" } } + +// NickTimer manages timing out of clients who are squatting reserved nicks +type NickTimer struct { + sync.Mutex // tier 1 + + // immutable after construction + timeout time.Duration + client *Client + + // mutable + nick string + accountForNick string + account string + timer *time.Timer +} + +// NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled) +func NewNickTimer(client *Client) *NickTimer { + config := client.server.AccountConfig() + if config.NickReservation != NickReservationWithTimeout { + return nil + } + nt := NickTimer{ + client: client, + timeout: config.NickReservationTimeout, + } + return &nt +} + +// Touch records a nick change and updates the timer as necessary +func (nt *NickTimer) Touch() { + if nt == nil { + return + } + + nick := nt.client.NickCasefolded() + account := nt.client.Account() + accountForNick := nt.client.server.accounts.NickToAccount(nick) + + var shouldWarn bool + + func() { + nt.Lock() + defer nt.Unlock() + // the timer will not reset as long as the squatter is targeting the same account + accountChanged := accountForNick != nt.accountForNick + // change state + nt.nick = nick + nt.account = account + nt.accountForNick = accountForNick + delinquent := accountForNick != "" && accountForNick != account + + if nt.timer != nil && (!delinquent || accountChanged) { + nt.timer.Stop() + nt.timer = nil + } + if delinquent && accountChanged { + nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout) + shouldWarn = true + } + }() + + if shouldWarn { + nt.sendWarning() + } +} + +func (nt *NickTimer) sendWarning() { + baseNotice := "Nickname is reserved; you must change it or authenticate to NickServ within %v" + nt.client.Notice(fmt.Sprintf(nt.client.t(baseNotice), nt.timeout)) +} + +func (nt *NickTimer) processTimeout() { + baseMsg := "Nick is reserved and authentication timeout expired: %v" + nt.client.Quit(fmt.Sprintf(nt.client.t(baseMsg), nt.timeout)) + nt.client.destroy(false) +} diff --git a/irc/nickname.go b/irc/nickname.go index df27975e..09409c82 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -17,6 +17,7 @@ var ( "=scene=": true, // used for rp commands "chanserv": true, "nickserv": true, + "hostserv": true, } ) @@ -45,11 +46,16 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s if err == errNicknameInUse { rb.Add(nil, server.name, ERR_NICKNAMEINUSE, client.nick, nickname, client.t("Nickname is already in use")) return false + } else if err == errNicknameReserved { + client.Send(nil, server.name, ERR_NICKNAMEINUSE, client.nick, nickname, client.t("Nickname is reserved by a different account")) + return false } else if err != nil { rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error())) return false } + client.nickTimer.Touch() + client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick)) if hadNick { target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), origNick, nickname)) diff --git a/irc/nickserv.go b/irc/nickserv.go index d92d813b..f9699ab8 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -4,16 +4,11 @@ package irc import ( - "encoding/json" "fmt" - "strconv" "strings" - "time" "github.com/goshuirc/irc-go/ircfmt" - "github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/sno" - "github.com/tidwall/buntdb" ) const nickservHelp = `NickServ lets you register and log into a user account. @@ -80,14 +75,14 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra return } - if !server.accountRegistration.Enabled { + if !server.AccountConfig().Registration.Enabled { rb.Notice(client.t("Account registration has been disabled")) return } if client.LoggedIntoAccount() { - if server.accountRegistration.AllowMultiplePerConnection { - client.LogoutOfAccount() + if server.AccountConfig().Registration.AllowMultiplePerConnection { + server.accounts.Logout(client) } else { rb.Notice(client.t("You're already logged into an account")) return @@ -103,26 +98,6 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra return } - // check whether account exists - // do it all in one write tx to prevent races - err = server.store.Update(func(tx *buntdb.Tx) error { - accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount) - - _, err := tx.Get(accountKey) - if err != buntdb.ErrNotFound { - //TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be - rb.Notice(client.t("Account already exists")) - return errAccountCreation - } - - registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) - - tx.Set(accountKey, "1", nil) - tx.Set(fmt.Sprintf(keyAccountName, casefoldedAccount), account, nil) - tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil) - return nil - }) - // account could not be created and relevant numerics have been dispatched, abort if err != nil { if err != errAccountCreation { @@ -131,87 +106,32 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra return } - // store details - err = server.store.Update(func(tx *buntdb.Tx) error { - // certfp special lookup key - if passphrase == "" { - assembledKeyCertToAccount := fmt.Sprintf(keyCertToAccount, client.certfp) - - // make sure certfp doesn't already exist because that'd be silly - _, err := tx.Get(assembledKeyCertToAccount) - if err != buntdb.ErrNotFound { - return errCertfpAlreadyExists - } - - tx.Set(assembledKeyCertToAccount, casefoldedAccount, nil) - } - - // make creds - var creds AccountCredentials - - // always set passphrase salt - creds.PassphraseSalt, err = passwd.NewSalt() - if err != nil { - return fmt.Errorf("Could not create passphrase salt: %s", err.Error()) - } - - if passphrase == "" { - creds.Certificate = client.certfp - } else { - creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase) - if err != nil { - return fmt.Errorf("Could not hash password: %s", err) - } - } - credText, err := json.Marshal(creds) - if err != nil { - return fmt.Errorf("Could not marshal creds: %s", err) - } - tx.Set(fmt.Sprintf(keyAccountCredentials, account), string(credText), nil) - - return nil - }) + err = server.accounts.Register(client, account, "", "", passphrase, client.certfp) + if err == nil { + err = server.accounts.Verify(client, casefoldedAccount, "") + } // details could not be stored and relevant numerics have been dispatched, abort if err != nil { errMsg := "Could not register" if err == errCertfpAlreadyExists { errMsg = "An account already exists for your certificate fingerprint" + } else if err == errAccountAlreadyRegistered { + errMsg = "Account already exists" } - rb.Notice(errMsg) - removeFailedAccRegisterData(server.store, casefoldedAccount) + rb.Notice(client.t(errMsg)) return } - err = server.store.Update(func(tx *buntdb.Tx) error { - tx.Set(fmt.Sprintf(keyAccountVerified, casefoldedAccount), "1", nil) - - // load acct info inside store tx - account := ClientAccount{ - Name: username, - RegisteredAt: time.Now(), - Clients: []*Client{client}, - } - //TODO(dan): Consider creating ircd-wide account adding/removing/affecting lock for protecting access to these sorts of variables - server.accounts[casefoldedAccount] = &account - client.account = &account - - rb.Notice(client.t("Account created")) - rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account.Name, fmt.Sprintf(client.t("You are now logged in as %s"), account.Name)) - rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful")) - server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), account.Name, client.nickMaskString)) - return nil - }) - if err != nil { - rb.Notice(client.t("Account registration failed")) - removeFailedAccRegisterData(server.store, casefoldedAccount) - return - } + rb.Notice(client.t("Account created")) + rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount)) + rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful")) + server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), casefoldedAccount, client.nickMaskString)) } func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) { // fail out if we need to - if !server.accountAuthenticationEnabled { + if !server.AccountConfig().AuthenticationEnabled { rb.Notice(client.t("Login has been disabled")) return } @@ -219,45 +139,13 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra // try passphrase if username != "" && passphrase != "" { // keep it the same as in the ACC CREATE stage - accountKey, err := CasefoldName(username) + accountName, err := CasefoldName(username) if err != nil { rb.Notice(client.t("Could not login with your username/password")) return } - // load and check acct data all in one update to prevent races. - // as noted elsewhere, change to proper locking for Account type later probably - var accountName string - err = server.store.Update(func(tx *buntdb.Tx) error { - // confirm account is verified - _, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey)) - if err != nil { - return errSaslFail - } - - creds, err := loadAccountCredentials(tx, accountKey) - if err != nil { - return err - } - - // ensure creds are valid - if len(creds.PassphraseHash) < 1 || len(creds.PassphraseSalt) < 1 || len(passphrase) < 1 { - return errSaslFail - } - err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, passphrase) - - // succeeded, load account info if necessary - account, exists := server.accounts[accountKey] - if !exists { - account = loadAccount(server, tx, accountKey) - } - - client.LoginToAccount(account) - accountName = account.Name - - return err - }) - + err = server.accounts.AuthenticateByPassphrase(client, accountName, passphrase) if err == nil { rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName)) return @@ -265,48 +153,11 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra } // try certfp - certfp := client.certfp - if certfp != "" { - var accountName string - err := server.store.Update(func(tx *buntdb.Tx) error { - // certfp lookup key - accountKey, err := tx.Get(fmt.Sprintf(keyCertToAccount, certfp)) - if err != nil { - return errSaslFail - } - - // confirm account exists - _, err = tx.Get(fmt.Sprintf(keyAccountExists, accountKey)) - if err != nil { - return errSaslFail - } - - // confirm account is verified - _, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey)) - if err != nil { - return errSaslFail - } - - // confirm the certfp in that account's credentials - creds, err := loadAccountCredentials(tx, accountKey) - if err != nil || creds.Certificate != client.certfp { - return errSaslFail - } - - // succeeded, load account info if necessary - account, exists := server.accounts[accountKey] - if !exists { - account = loadAccount(server, tx, accountKey) - } - - client.LoginToAccount(account) - accountName = account.Name - - return nil - }) - + if client.certfp != "" { + err := server.accounts.AuthenticateByCertFP(client) if err == nil { - rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName)) + rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName())) + // TODO more notices? return } } diff --git a/irc/responsebuffer.go b/irc/responsebuffer.go index 07ef0523..a1071e53 100644 --- a/irc/responsebuffer.go +++ b/irc/responsebuffer.go @@ -43,11 +43,11 @@ func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, c // AddFromClient adds a new message from a specific client to our queue. func (rb *ResponseBuffer) AddFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) { // attach account-tag - if rb.target.capabilities.Has(caps.AccountTag) && from.account != &NoAccount { + if rb.target.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() { if tags == nil { - tags = ircmsg.MakeTags("account", from.account.Name) + tags = ircmsg.MakeTags("account", from.AccountName()) } else { - (*tags)["account"] = ircmsg.MakeTagValue(from.account.Name) + (*tags)["account"] = ircmsg.MakeTagValue(from.AccountName()) } } // attach message-id diff --git a/irc/server.go b/irc/server.go index f053b285..051a36c7 100644 --- a/irc/server.go +++ b/irc/server.go @@ -87,49 +87,48 @@ type ListenerWrapper struct { // Server is the main Oragono server. type Server struct { - accountAuthenticationEnabled bool - accountRegistration *AccountRegistration - accounts map[string]*ClientAccount - batches *BatchManager - channelRegistrationEnabled bool - channels *ChannelManager - channelRegistry *ChannelRegistry - checkIdent bool - clients *ClientManager - configFilename string - configurableStateMutex sync.RWMutex // tier 1; generic protection for server state modified by rehash() - connectionLimiter *connection_limits.Limiter - connectionThrottler *connection_limits.Throttler - ctime time.Time - defaultChannelModes modes.Modes - dlines *DLineManager - loggingRawIO bool - isupport *isupport.List - klines *KLineManager - languages *languages.Manager - limits Limits - listeners map[string]*ListenerWrapper - logger *logger.Manager - MaxSendQBytes uint64 - monitorManager *MonitorManager - motdLines []string - name string - nameCasefolded string - networkName string - operators map[string]Oper - operclasses map[string]OperClass - password []byte - passwords *passwd.SaltedManager - recoverFromErrors bool - rehashMutex sync.Mutex // tier 3 - rehashSignal chan os.Signal - proxyAllowedFrom []string - signals chan os.Signal - snomasks *SnoManager - store *buntdb.DB - stsEnabled bool - webirc []webircConfig - whoWas *WhoWasList + accountConfig *AccountConfig + accounts *AccountManager + batches *BatchManager + channelRegistrationEnabled bool + channels *ChannelManager + channelRegistry *ChannelRegistry + checkIdent bool + clients *ClientManager + configFilename string + configurableStateMutex sync.RWMutex // tier 1; generic protection for server state modified by rehash() + connectionLimiter *connection_limits.Limiter + connectionThrottler *connection_limits.Throttler + ctime time.Time + defaultChannelModes modes.Modes + dlines *DLineManager + loggingRawIO bool + isupport *isupport.List + klines *KLineManager + languages *languages.Manager + limits Limits + listeners map[string]*ListenerWrapper + logger *logger.Manager + MaxSendQBytes uint64 + monitorManager *MonitorManager + motdLines []string + name string + nameCasefolded string + networkName string + operators map[string]Oper + operclasses map[string]OperClass + password []byte + passwords *passwd.SaltedManager + recoverFromErrors bool + rehashMutex sync.Mutex // tier 4 + rehashSignal chan os.Signal + proxyAllowedFrom []string + signals chan os.Signal + snomasks *SnoManager + store *buntdb.DB + stsEnabled bool + webirc []webircConfig + whoWas *WhoWasList } var ( @@ -150,7 +149,6 @@ type clientConn struct { func NewServer(config *Config, logger *logger.Manager) (*Server, error) { // initialize data structures server := &Server{ - accounts: make(map[string]*ClientAccount), batches: NewBatchManager(), channels: NewChannelManager(), clients: NewClientManager(), @@ -214,10 +212,10 @@ func (server *Server) setISupport() { isupport.Add("UTF8MAPPING", casemappingName) // account registration - if server.accountRegistration.Enabled { + if server.accountConfig.Registration.Enabled { // 'none' isn't shown in the REGCALLBACKS vars var enabledCallbacks []string - for _, name := range server.accountRegistration.EnabledCallbacks { + for _, name := range server.accountConfig.Registration.EnabledCallbacks { if name != "*" { enabledCallbacks = append(enabledCallbacks, name) } @@ -348,15 +346,8 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config) *Listen // make listener var listener net.Listener var err error - optionalUnixPrefix := "unix:" - optionalPrefixLen := len(optionalUnixPrefix) - if len(addr) >= optionalPrefixLen && strings.ToLower(addr[0:optionalPrefixLen]) == optionalUnixPrefix { - addr = addr[optionalPrefixLen:] - if len(addr) == 0 || addr[0] != '/' { - log.Fatal("Bad unix socket address", addr) - } - } - if len(addr) > 0 && addr[0] == '/' { + addr = strings.TrimPrefix(addr, "unix:") + if strings.HasPrefix(addr, "/") { // https://stackoverflow.com/a/34881585 os.Remove(addr) listener, err = net.Listen("unix", addr) @@ -478,7 +469,7 @@ func (server *Server) tryRegister(c *Client) { } if c.capabilities.Has(caps.ExtendedJoin) { - c.Send(nil, c.nickMaskString, "JOIN", channel.name, c.account.Name, c.realname) + c.Send(nil, c.nickMaskString, "JOIN", channel.name, c.AccountName(), c.realname) } else { c.Send(nil, c.nickMaskString, "JOIN", channel.name) } @@ -630,9 +621,8 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) { if target.flags[modes.TLS] { rb.Add(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, client.t("is using a secure connection")) } - accountName := target.AccountName() - if accountName != "" { - rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, accountName, client.t("is logged in as")) + if target.LoggedIntoAccount() { + rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, client.AccountName(), client.t("is logged in as")) } if target.flags[modes.Bot] { rb.Add(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.networkName))) @@ -803,18 +793,28 @@ func (server *Server) applyConfig(config *Config, initial bool) error { server.languages = lm // SASL - if config.Accounts.AuthenticationEnabled && !server.accountAuthenticationEnabled { + oldAccountConfig := server.AccountConfig() + authPreviouslyEnabled := oldAccountConfig != nil && !oldAccountConfig.AuthenticationEnabled + if config.Accounts.AuthenticationEnabled && !authPreviouslyEnabled { // enabling SASL SupportedCapabilities.Enable(caps.SASL) CapValues.Set(caps.SASL, "PLAIN,EXTERNAL") addedCaps.Add(caps.SASL) - } - if !config.Accounts.AuthenticationEnabled && server.accountAuthenticationEnabled { + } else if !config.Accounts.AuthenticationEnabled && authPreviouslyEnabled { // disabling SASL SupportedCapabilities.Disable(caps.SASL) removedCaps.Add(caps.SASL) } - server.accountAuthenticationEnabled = config.Accounts.AuthenticationEnabled + + server.configurableStateMutex.Lock() + server.accountConfig = &config.Accounts + server.configurableStateMutex.Unlock() + + nickReservationPreviouslyDisabled := oldAccountConfig != nil && oldAccountConfig.NickReservation == NickReservationDisabled + nickReservationNowEnabled := config.Accounts.NickReservation != NickReservationDisabled + if nickReservationPreviouslyDisabled && nickReservationNowEnabled { + server.accounts.buildNickToAccountIndex() + } // STS stsValue := config.Server.STS.Value() @@ -902,8 +902,6 @@ func (server *Server) applyConfig(config *Config, initial bool) error { server.checkIdent = config.Server.CheckIdent // registration - accountReg := NewAccountRegistration(config.Accounts.Registration) - server.accountRegistration = &accountReg server.channelRegistrationEnabled = config.Channels.Registration.Enabled server.defaultChannelModes = ParseDefaultChannelModes(config) @@ -1043,6 +1041,8 @@ func (server *Server) loadDatastore(datastorePath string) error { server.channelRegistry = NewChannelRegistry(server) + server.accounts = NewAccountManager(server) + return nil } diff --git a/oragono.yaml b/oragono.yaml index bebdce35..8d614aae 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -159,6 +159,14 @@ accounts: # is account authentication enabled? authentication-enabled: true + # will the server enforce that only the account holder can use the account name as a nick? + # options: + # `disabled`: no enforcement + # `timeout` (auth to nickserv within some period of time or you're disconnected) + # `strict`: must authenticate up front with SASL + nick-reservation: disabled + nick-reservation-timeout: 30s + # channel options channels: # modes that are set when new channels are created @@ -210,6 +218,7 @@ oper-classes: capabilities: - "oper:rehash" - "oper:die" + - "unregister" - "samode" # ircd operators