3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-10 22:19:31 +01:00

Merge pull request #206 from slingamn/ghost.2

nickserv: implement GHOST, GROUP, DROP, and INFO
This commit is contained in:
Shivaram Lingamneni 2018-03-14 09:42:20 -04:00 committed by GitHub
commit b0f262bc0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 369 additions and 104 deletions

View File

@ -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]
@ -149,30 +161,40 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
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 {
err = func() error {
am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock()
// can't register an account with the same name as a registered nick
if am.NickToAccount(casefoldedAccount) != "" {
return errAccountAlreadyRegistered
}
if certfp != "" {
// make sure certfp doesn't already exist because that'd be silly
_, err := tx.Get(certFPKey)
if err != buntdb.ErrNotFound {
return errCertfpAlreadyExists
return am.server.store.Update(func(tx *buntdb.Tx) error {
_, err := am.loadRawAccount(tx, casefoldedAccount)
if err != errAccountDoesNotExist {
return errAccountAlreadyRegistered
}
}
tx.Set(accountKey, "1", setOptions)
tx.Set(accountNameKey, account, setOptions)
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
tx.Set(credentialsKey, credStr, setOptions)
tx.Set(callbackKey, callbackSpec, setOptions)
if certfp != "" {
tx.Set(certFPKey, casefoldedAccount, setOptions)
}
return nil
})
if certfp != "" {
// make sure certfp doesn't already exist because that'd be silly
_, err := tx.Get(certFPKey)
if err != buntdb.ErrNotFound {
return errCertfpAlreadyExists
}
}
tx.Set(accountKey, "1", setOptions)
tx.Set(accountNameKey, account, setOptions)
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
tx.Set(credentialsKey, credStr, setOptions)
tx.Set(callbackKey, callbackSpec, setOptions)
if certfp != "" {
tx.Set(certFPKey, casefoldedAccount, setOptions)
}
return nil
})
}()
if err != nil {
return err
@ -325,13 +347,110 @@ 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)
if err != nil {
return errAccountDoesNotExist
func marshalReservedNicks(nicks []string) string {
return strings.Join(nicks, ",")
}
func unmarshalReservedNicks(nicks string) (result []string) {
if nicks == "" {
return
}
return strings.Split(nicks, ",")
}
func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreserve bool, reserve bool) error {
cfnick, err := CasefoldName(nick)
// garbage nick, or garbage options, or disabled
nrconfig := am.server.AccountConfig().NickReservation
if err != nil || cfnick == "" || (reserve && saUnreserve) || !nrconfig.Enabled {
return errAccountNickReservationFailed
}
account, err := am.LoadAccount(casefoldedAccount)
// the cache is in sync with the DB while we hold serialCacheUpdateMutex
am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock()
// find the affected account, which is usually the client's:
account := client.Account()
if saUnreserve {
// unless this is a sadrop:
account = am.NickToAccount(cfnick)
if account == "" {
// nothing to do
return nil
}
}
if account == "" {
return errAccountNotLoggedIn
}
accountForNick := am.NickToAccount(cfnick)
if reserve && accountForNick != "" {
return errNicknameReserved
} else if !reserve && !saUnreserve && accountForNick != account {
return errNicknameReserved
} else if !reserve && cfnick == account {
return errAccountCantDropPrimaryNick
}
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, account)
unverifiedAccountKey := fmt.Sprintf(keyAccountExists, cfnick)
err = am.server.store.Update(func(tx *buntdb.Tx) error {
if reserve {
// unverified accounts don't show up in NickToAccount yet (which is intentional),
// however you shouldn't be able to reserve a nick out from under them
_, err := tx.Get(unverifiedAccountKey)
if err == nil {
return errNicknameReserved
}
}
rawNicks, err := tx.Get(nicksKey)
if err != nil && err != buntdb.ErrNotFound {
return err
}
nicks := unmarshalReservedNicks(rawNicks)
if reserve {
if len(nicks) >= nrconfig.AdditionalNickLimit {
return errAccountTooManyNicks
}
nicks = append(nicks, 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 || err == errNicknameReserved {
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 +469,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 +494,7 @@ func (am *AccountManager) LoadAccount(casefoldedAccount string) (result ClientAc
err = errAccountDoesNotExist
return
}
result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
result.Verified = raw.Verified
return
}
@ -380,6 +506,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 +518,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 +540,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 +631,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 +688,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", "*")
}
}()
}

View File

@ -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.

View File

@ -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")

View File

@ -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)))

View File

@ -179,6 +179,7 @@ type NickTimer struct {
client *Client
// mutable
stopped bool
nick string
accountForNick string
account string
@ -213,6 +214,11 @@ func (nt *NickTimer) Touch() {
func() {
nt.Lock()
defer nt.Unlock()
if nt.stopped {
return
}
// the timer will not reset as long as the squatter is targeting the same account
accountChanged := accountForNick != nt.accountForNick
// change state
@ -248,6 +254,7 @@ func (nt *NickTimer) Stop() {
nt.timer.Stop()
nt.timer = nil
}
nt.stopped = true
}
func (nt *NickTimer) sendWarning() {

View File

@ -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,20 @@ 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, false, rb)
} else if command == "sadrop" {
nick, _ := extractParam(params)
server.nickservDropHandler(client, nick, true, rb)
} else {
rb.Notice(client.t("Command not recognised. To see the available commands, run /NS HELP"))
}
@ -158,7 +183,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 +258,103 @@ 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, false, 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 by someone else"))
} 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, sadrop bool, rb *ResponseBuffer) {
if sadrop {
if !client.HasRoleCapabs("unregister") {
rb.Notice(client.t("Insufficient oper privs"))
return
}
}
err := server.accounts.SetNickReserved(client, nick, sadrop, false)
if err == nil {
rb.Notice(fmt.Sprintf(client.t("Successfully ungrouped nick %s with your account"), nick))
} else if err == errAccountNotLoggedIn {
rb.Notice(fmt.Sprintf(client.t("You're not logged into an account")))
} 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 == errNicknameReserved {
rb.Notice(fmt.Sprintf(client.t("That nickname is already reserved by someone else")))
} else {
rb.Notice(client.t("Error ungrouping nick"))
}
}

View File

@ -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