mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 13:29:27 +01:00
nickserv: implement GHOST, GROUP, DROP, and INFO
This commit is contained in:
parent
b211fd35da
commit
a022befffe
166
irc/accounts.go
166
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,11 +512,12 @@ 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 rawNicks string
|
||||
|
||||
am.serialCacheUpdateMutex.Lock()
|
||||
defer am.serialCacheUpdateMutex.Unlock()
|
||||
@ -428,6 +529,8 @@ func (am *AccountManager) Unregister(account string) error {
|
||||
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
|
||||
@ -446,17 +549,19 @@ func (am *AccountManager) Unregister(account string) error {
|
||||
}
|
||||
}
|
||||
|
||||
additionalNicks := unmarshalReservedNicks(rawNicks)
|
||||
|
||||
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 _, 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)
|
||||
@ -562,6 +663,7 @@ type ClientAccount struct {
|
||||
RegisteredAt time.Time
|
||||
Credentials AccountCredentials
|
||||
Verified bool
|
||||
AdditionalNicks []string
|
||||
}
|
||||
|
||||
// convenience for passing around raw serialized account data
|
||||
@ -571,30 +673,32 @@ type rawClientAccount struct {
|
||||
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
|
||||
go func() {
|
||||
for friend := range client.Friends(caps.AccountNotify) {
|
||||
friend.Send(nil, client.nickMaskString, "ACCOUNT", "*")
|
||||
friend.Send(nil, client.NickMaskString(), "ACCOUNT", "*")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
|
@ -118,6 +118,7 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
|
||||
|
||||
type NickReservationConfig struct {
|
||||
Enabled bool
|
||||
AdditionalNickLimit int `yaml:"additional-nick-limit"`
|
||||
Method NickReservationMethod
|
||||
RenameTimeout time.Duration `yaml:"rename-timeout"`
|
||||
RenamePrefix string `yaml:"rename-prefix"`
|
||||
|
@ -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")
|
||||
|
@ -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)))
|
||||
|
123
irc/nickserv.go
123
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"))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user