diff --git a/irc/accounts.go b/irc/accounts.go index 040f8be8..f9f2cea4 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -29,6 +29,7 @@ const ( 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" keyCertToAccount = "account.creds.certfp %s" ) @@ -75,6 +76,12 @@ func (am *AccountManager) buildNickToAccountIndex() { if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil { result[accountName] = accountName } + if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, accountName)); err == nil { + additionalNicks := unmarshalReservedNicks(rawNicks) + for _, nick := range additionalNicks { + result[nick] = accountName + } + } return true }) return err @@ -91,7 +98,12 @@ func (am *AccountManager) buildNickToAccountIndex() { return } -func (am *AccountManager) NickToAccount(cfnick string) string { +func (am *AccountManager) NickToAccount(nick string) string { + cfnick, err := CasefoldName(nick) + if err != nil { + return "" + } + am.RLock() defer am.RUnlock() return am.nickToAccount[cfnick] @@ -325,13 +337,92 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er return nil } -func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error { - casefoldedAccount, err := CasefoldName(accountName) +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, reserve bool) error { + cfnick, err := CasefoldName(nick) if err != nil { - return errAccountDoesNotExist + return errAccountNickReservationFailed } - account, err := am.LoadAccount(casefoldedAccount) + // sanity check so we don't persist bad data + account := client.Account() + if account == "" || cfnick == "" || !am.server.AccountConfig().NickReservation.Enabled { + return errAccountNickReservationFailed + } + + limit := am.server.AccountConfig().NickReservation.AdditionalNickLimit + + am.serialCacheUpdateMutex.Lock() + defer am.serialCacheUpdateMutex.Unlock() + + // the cache is in sync with the DB while we hold serialCacheUpdateMutex + accountForNick := am.NickToAccount(cfnick) + if reserve && accountForNick != "" { + return errNicknameReserved + } else if !reserve && accountForNick != account { + return errAccountNickReservationFailed + } else if !reserve && cfnick == account { + return errAccountCantDropPrimaryNick + } + + nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, account) + err = am.server.store.Update(func(tx *buntdb.Tx) error { + rawNicks, err := tx.Get(nicksKey) + if err != nil && err != buntdb.ErrNotFound { + return err + } + + nicks := unmarshalReservedNicks(rawNicks) + + if reserve { + if len(nicks) >= limit { + return errAccountTooManyNicks + } + nicks = append(nicks, cfnick) + } else { + var newNicks []string + for _, reservedNick := range nicks { + if reservedNick != cfnick { + newNicks = append(newNicks, reservedNick) + } + } + nicks = newNicks + } + + marshaledNicks := marshalReservedNicks(nicks) + _, _, err = tx.Set(nicksKey, string(marshaledNicks), nil) + return err + }) + + if err == errAccountTooManyNicks { + return err + } else if err != nil { + return errAccountNickReservationFailed + } + + // success + am.Lock() + defer am.Unlock() + if reserve { + am.nickToAccount[cfnick] = account + } else { + delete(am.nickToAccount, cfnick) + } + return nil +} + +func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error { + account, err := am.LoadAccount(accountName) if err != nil { return err } @@ -350,7 +441,13 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s return nil } -func (am *AccountManager) LoadAccount(casefoldedAccount string) (result ClientAccount, err error) { +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) @@ -369,6 +466,7 @@ func (am *AccountManager) LoadAccount(casefoldedAccount string) (result ClientAc err = errAccountDoesNotExist return } + result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks) result.Verified = raw.Verified return } @@ -380,6 +478,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount) + nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) _, e := tx.Get(accountKey) if e == buntdb.ErrNotFound { @@ -391,6 +490,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string result.RegisteredAt, _ = tx.Get(registeredTimeKey) result.Credentials, _ = tx.Get(credentialsKey) result.Callback, _ = tx.Get(callbackKey) + result.AdditionalNicks, _ = tx.Get(nicksKey) if _, e = tx.Get(verifiedKey); e == nil { result.Verified = true @@ -412,51 +512,56 @@ func (am *AccountManager) Unregister(account string) error { callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount) verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) + nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) var clients []*Client - func() { - var credText string + var credText string + var rawNicks string - am.serialCacheUpdateMutex.Lock() - defer am.serialCacheUpdateMutex.Unlock() + 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) - tx.Delete(callbackKey) - tx.Delete(verificationCodeKey) - credText, err = tx.Get(credentialsKey) - tx.Delete(credentialsKey) - return nil - }) + am.server.store.Update(func(tx *buntdb.Tx) error { + tx.Delete(accountKey) + tx.Delete(accountNameKey) + tx.Delete(verifiedKey) + tx.Delete(registeredTimeKey) + tx.Delete(callbackKey) + tx.Delete(verificationCodeKey) + rawNicks, _ = tx.Get(nicksKey) + tx.Delete(nicksKey) + 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 - }) - } + 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) - }() + additionalNicks := unmarshalReservedNicks(rawNicks) + am.Lock() + defer am.Unlock() + + clients = am.accountToClients[casefoldedAccount] + delete(am.accountToClients, casefoldedAccount) + delete(am.nickToAccount, casefoldedAccount) + for _, nick := range additionalNicks { + delete(am.nickToAccount, nick) + } for _, client := range clients { - client.LogoutOfAccount() + am.logoutOfAccount(client) } if err != nil { @@ -498,29 +603,25 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error { } func (am *AccountManager) Login(client *Client, account string) { - client.LoginToAccount(account) - - casefoldedAccount, _ := CasefoldName(account) am.Lock() defer am.Unlock() + + am.loginToAccount(client, account) + casefoldedAccount := client.Account() 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() { + casefoldedAccount := client.Account() + if casefoldedAccount == "" { return } + am.logoutOfAccount(client) + clients := am.accountToClients[casefoldedAccount] if len(clients) <= 1 { delete(am.accountToClients, casefoldedAccount) @@ -559,42 +660,45 @@ type ClientAccount struct { // Name of the account. Name string // RegisteredAt represents the time that the account was registered. - RegisteredAt time.Time - Credentials AccountCredentials - Verified bool + RegisteredAt time.Time + Credentials AccountCredentials + Verified bool + AdditionalNicks []string } // convenience for passing around raw serialized account data type rawClientAccount struct { - Name string - RegisteredAt string - Credentials string - Callback string - Verified bool + Name string + RegisteredAt string + Credentials string + Callback string + Verified bool + AdditionalNicks string } -// LoginToAccount logs the client into the given account. -func (client *Client) LoginToAccount(account string) { +// loginToAccount logs the client into the given account. +func (am *AccountManager) loginToAccount(client *Client, account string) { changed := client.SetAccountName(account) if changed { - client.nickTimer.Touch() + go client.nickTimer.Touch() } } -// LogoutOfAccount logs the client out of their current account. -func (client *Client) LogoutOfAccount() { +// logoutOfAccount logs the client out of their current account. +func (am *AccountManager) logoutOfAccount(client *Client) { if client.Account() == "" { // already logged out return } client.SetAccountName("") - client.nickTimer.Touch() + go client.nickTimer.Touch() // dispatch account-notify // TODO: doing the I/O here is kind of a kludge, let's move this somewhere else - for friend := range client.Friends(caps.AccountNotify) { - friend.Send(nil, client.nickMaskString, "ACCOUNT", "*") - } + go func() { + for friend := range client.Friends(caps.AccountNotify) { + friend.Send(nil, client.NickMaskString(), "ACCOUNT", "*") + } + }() } - diff --git a/irc/config.go b/irc/config.go index a5600663..ba899081 100644 --- a/irc/config.go +++ b/irc/config.go @@ -117,10 +117,11 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error } type NickReservationConfig struct { - Enabled bool - Method NickReservationMethod - RenameTimeout time.Duration `yaml:"rename-timeout"` - RenamePrefix string `yaml:"rename-prefix"` + Enabled bool + AdditionalNickLimit int `yaml:"additional-nick-limit"` + Method NickReservationMethod + RenameTimeout time.Duration `yaml:"rename-timeout"` + RenamePrefix string `yaml:"rename-prefix"` } // ChannelRegistrationConfig controls channel registration. diff --git a/irc/errors.go b/irc/errors.go index 4158480c..c6a5fe87 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -12,11 +12,15 @@ var ( errAccountAlreadyRegistered = errors.New("Account already exists") errAccountCreation = errors.New("Account could not be created") errAccountDoesNotExist = errors.New("Account does not exist") + errAccountNotLoggedIn = errors.New("You're not logged into an account") errAccountVerificationFailed = errors.New("Account verification failed") errAccountVerificationInvalidCode = errors.New("Invalid account verification code") errAccountUnverified = errors.New("Account is not yet verified") errAccountAlreadyVerified = errors.New("Account is already verified") errAccountInvalidCredentials = errors.New("Invalid account credentials") + errAccountTooManyNicks = errors.New("Account has too many reserved nicks") + errAccountNickReservationFailed = errors.New("Could not (un)reserve nick") + errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname") errCallbackFailed = errors.New("Account verification could not be sent") errCertfpAlreadyExists = errors.New("An account already exists with your certificate") errChannelAlreadyRegistered = errors.New("Channel is already registered") diff --git a/irc/handlers.go b/irc/handlers.go index 50c77513..c25fe7ce 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -352,15 +352,8 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value [] return false } - // keep it the same as in the REG CREATE stage - accountKey, err := CasefoldName(accountKey) - if err != nil { - rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: Bad account name")) - return false - } - password := string(splitValue[2]) - err = server.accounts.AuthenticateByPassphrase(client, accountKey, password) + err := server.accounts.AuthenticateByPassphrase(client, accountKey, password) if err != nil { 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))) diff --git a/irc/nickserv.go b/irc/nickserv.go index d2a20b9d..d3f61795 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -28,7 +28,18 @@ Leave out [username] if you're unregistering the user you're currently logged in To login to an account: /NS IDENTIFY [username password] Leave out [username password] to use your client certificate fingerprint. Otherwise, -the given username and password will be used.` +the given username and password will be used. + +To see account information: + /NS INFO [username] +Leave out [username] to see your own account information. + +To associate your current nick with the account you're logged into: + /NS GROUP + +To disassociate a nick with the account you're logged into: + /NS DROP [nickname] +Leave out [nickname] to drop your association with your current nickname.` // extractParam extracts a parameter from the given string, returning the param and the rest of the string. func extractParam(line string) (string, string) { @@ -69,6 +80,17 @@ func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb } else if command == "unregister" { username, _ := extractParam(params) server.nickservUnregisterHandler(client, username, rb) + } else if command == "ghost" { + nick, _ := extractParam(params) + server.nickservGhostHandler(client, nick, rb) + } else if command == "info" { + nick, _ := extractParam(params) + server.nickservInfoHandler(client, nick, rb) + } else if command == "group" { + server.nickservGroupHandler(client, rb) + } else if command == "drop" { + nick, _ := extractParam(params) + server.nickservDropHandler(client, nick, rb) } else { rb.Notice(client.t("Command not recognised. To see the available commands, run /NS HELP")) } @@ -158,7 +180,7 @@ func (server *Server) nickservRegisterHandler(client *Client, username, email, p config := server.AccountConfig() var callbackNamespace, callbackValue string noneCallbackAllowed := false - for _, callback := range(config.Registration.EnabledCallbacks) { + for _, callback := range config.Registration.EnabledCallbacks { if callback == "*" { noneCallbackAllowed = true } @@ -233,3 +255,100 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password")) } } + +func (server *Server) nickservGhostHandler(client *Client, nick string, rb *ResponseBuffer) { + if !server.AccountConfig().NickReservation.Enabled { + rb.Notice(client.t("Nickname reservation is disabled")) + return + } + + account := client.Account() + if account == "" || server.accounts.NickToAccount(nick) != account { + rb.Notice(client.t("You don't own that nick")) + return + } + + ghost := server.clients.Get(nick) + if ghost == nil { + rb.Notice(client.t("No such nick")) + return + } else if ghost == client { + rb.Notice(client.t("You can't GHOST yourself (try /QUIT instead)")) + return + } + + ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick())) + ghost.destroy(false) +} + +func (server *Server) nickservGroupHandler(client *Client, rb *ResponseBuffer) { + if !server.AccountConfig().NickReservation.Enabled { + rb.Notice(client.t("Nickname reservation is disabled")) + return + } + + account := client.Account() + if account == "" { + rb.Notice(client.t("You're not logged into an account")) + return + } + + nick := client.NickCasefolded() + err := server.accounts.SetNickReserved(client, nick, true) + if err == nil { + rb.Notice(fmt.Sprintf(client.t("Successfully grouped nick %s with your account"), nick)) + } else if err == errAccountTooManyNicks { + rb.Notice(client.t("You have too many nicks reserved already (you can remove some with /NS DROP)")) + } else if err == errNicknameReserved { + rb.Notice(client.t("That nickname is already reserved")) + } else { + rb.Notice(client.t("Error reserving nickname")) + } +} + +func (server *Server) nickservInfoHandler(client *Client, nick string, rb *ResponseBuffer) { + if nick == "" { + nick = client.Nick() + } + + accountName := nick + if server.AccountConfig().NickReservation.Enabled { + accountName = server.accounts.NickToAccount(nick) + if accountName == "" { + rb.Notice(client.t("That nickname is not registered")) + return + } + } + + account, err := server.accounts.LoadAccount(accountName) + if err != nil || !account.Verified { + rb.Notice(client.t("Account does not exist")) + } + + rb.Notice(fmt.Sprintf(client.t("Account: %s"), account.Name)) + registeredAt := account.RegisteredAt.Format("Jan 02, 2006 15:04:05Z") + rb.Notice(fmt.Sprintf(client.t("Registered at: %s"), registeredAt)) + // TODO nicer formatting for this + for _, nick := range account.AdditionalNicks { + rb.Notice(fmt.Sprintf(client.t("Additional grouped nick: %s"), nick)) + } +} + +func (server *Server) nickservDropHandler(client *Client, nick string, rb *ResponseBuffer) { + account := client.Account() + if account == "" { + rb.Notice(client.t("You're not logged into an account")) + return + } + + err := server.accounts.SetNickReserved(client, nick, false) + if err == nil { + rb.Notice(fmt.Sprintf(client.t("Successfully ungrouped nick %s with your account"), nick)) + } else if err == errAccountCantDropPrimaryNick { + rb.Notice(fmt.Sprintf(client.t("You can't ungroup your primary nickname (try unregistering your account instead)"))) + } else if err == errAccountNickReservationFailed { + rb.Notice(fmt.Sprintf(client.t("You don't own that nick"))) + } else { + rb.Notice(client.t("Error ungrouping nick")) + } +} diff --git a/oragono.yaml b/oragono.yaml index 76d11980..e49119c1 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -182,6 +182,9 @@ accounts: # is there any enforcement of reserved nicknames? enabled: false + # how many nicknames, in addition to the account name, can be reserved? + additional-nick-limit: 2 + # method describes how nickname reservation is handled # timeout: let the user change to the registered nickname, give them X seconds # to login and then rename them if they haven't done so