mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 18:52:41 +01:00
use the TR39 skeleton algorithm to prevent confusables (#178)
This commit is contained in:
parent
a11486d699
commit
b9b2553a2f
130
irc/accounts.go
130
irc/accounts.go
@ -52,17 +52,19 @@ type AccountManager struct {
|
|||||||
|
|
||||||
server *Server
|
server *Server
|
||||||
// track clients logged in to accounts
|
// track clients logged in to accounts
|
||||||
accountToClients map[string][]*Client
|
accountToClients map[string][]*Client
|
||||||
nickToAccount map[string]string
|
nickToAccount map[string]string
|
||||||
accountToMethod map[string]NickReservationMethod
|
skeletonToAccount map[string]string
|
||||||
|
accountToMethod map[string]NickReservationMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAccountManager(server *Server) *AccountManager {
|
func NewAccountManager(server *Server) *AccountManager {
|
||||||
am := AccountManager{
|
am := AccountManager{
|
||||||
accountToClients: make(map[string][]*Client),
|
accountToClients: make(map[string][]*Client),
|
||||||
nickToAccount: make(map[string]string),
|
nickToAccount: make(map[string]string),
|
||||||
accountToMethod: make(map[string]NickReservationMethod),
|
skeletonToAccount: make(map[string]string),
|
||||||
server: server,
|
accountToMethod: make(map[string]NickReservationMethod),
|
||||||
|
server: server,
|
||||||
}
|
}
|
||||||
|
|
||||||
am.buildNickToAccountIndex()
|
am.buildNickToAccountIndex()
|
||||||
@ -76,6 +78,7 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nickToAccount := make(map[string]string)
|
nickToAccount := make(map[string]string)
|
||||||
|
skeletonToAccount := make(map[string]string)
|
||||||
accountToMethod := make(map[string]NickReservationMethod)
|
accountToMethod := make(map[string]NickReservationMethod)
|
||||||
existsPrefix := fmt.Sprintf(keyAccountExists, "")
|
existsPrefix := fmt.Sprintf(keyAccountExists, "")
|
||||||
|
|
||||||
@ -91,11 +94,21 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
|||||||
account := strings.TrimPrefix(key, existsPrefix)
|
account := strings.TrimPrefix(key, existsPrefix)
|
||||||
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil {
|
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil {
|
||||||
nickToAccount[account] = account
|
nickToAccount[account] = account
|
||||||
|
accountName, err := tx.Get(fmt.Sprintf(keyAccountName, account))
|
||||||
|
if err != nil {
|
||||||
|
am.server.logger.Error("internal", "missing account name for", account)
|
||||||
|
} else {
|
||||||
|
skeleton, _ := Skeleton(accountName)
|
||||||
|
skeletonToAccount[skeleton] = account
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, account)); err == nil {
|
if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, account)); err == nil {
|
||||||
additionalNicks := unmarshalReservedNicks(rawNicks)
|
additionalNicks := unmarshalReservedNicks(rawNicks)
|
||||||
for _, nick := range additionalNicks {
|
for _, nick := range additionalNicks {
|
||||||
nickToAccount[nick] = account
|
cfnick, _ := CasefoldName(nick)
|
||||||
|
nickToAccount[cfnick] = account
|
||||||
|
skeleton, _ := Skeleton(nick)
|
||||||
|
skeletonToAccount[skeleton] = account
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,6 +128,7 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
|||||||
} else {
|
} else {
|
||||||
am.Lock()
|
am.Lock()
|
||||||
am.nickToAccount = nickToAccount
|
am.nickToAccount = nickToAccount
|
||||||
|
am.skeletonToAccount = skeletonToAccount
|
||||||
am.accountToMethod = accountToMethod
|
am.accountToMethod = accountToMethod
|
||||||
am.Unlock()
|
am.Unlock()
|
||||||
}
|
}
|
||||||
@ -171,36 +185,55 @@ func (am *AccountManager) NickToAccount(nick string) string {
|
|||||||
|
|
||||||
// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
|
// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
|
||||||
// used to enforce ownership.
|
// used to enforce ownership.
|
||||||
func (am *AccountManager) EnforcementStatus(nick string) (account string, method NickReservationMethod) {
|
func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickReservationMethod) {
|
||||||
cfnick, err := CasefoldName(nick)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config := am.server.Config()
|
config := am.server.Config()
|
||||||
if !config.Accounts.NickReservation.Enabled {
|
if !config.Accounts.NickReservation.Enabled {
|
||||||
method = NickReservationNone
|
return "", NickReservationNone
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
am.RLock()
|
am.RLock()
|
||||||
defer am.RUnlock()
|
defer am.RUnlock()
|
||||||
|
|
||||||
account = am.nickToAccount[cfnick]
|
// given an account, combine stored enforcement method with the config settings
|
||||||
if account == "" {
|
// to compute the actual enforcement method
|
||||||
method = NickReservationNone
|
finalEnforcementMethod := func(account_ string) (result NickReservationMethod) {
|
||||||
|
result = am.accountToMethod[account_]
|
||||||
|
// if they don't have a custom setting, or customization is disabled, use the default
|
||||||
|
if result == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
|
||||||
|
result = config.Accounts.NickReservation.Method
|
||||||
|
}
|
||||||
|
if result == NickReservationOptional {
|
||||||
|
// enforcement was explicitly enabled neither in the config or by the user
|
||||||
|
result = NickReservationNone
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
method = am.accountToMethod[account]
|
|
||||||
// if they don't have a custom setting, or customization is disabled, use the default
|
nickAccount := am.nickToAccount[cfnick]
|
||||||
if method == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
|
skelAccount := am.skeletonToAccount[skeleton]
|
||||||
method = config.Accounts.NickReservation.Method
|
if nickAccount == "" && skelAccount == "" {
|
||||||
|
return "", NickReservationNone
|
||||||
|
} else if nickAccount != "" && skelAccount != "" && nickAccount != skelAccount {
|
||||||
|
// two people have competing claims on (this casefolding of) this nick!
|
||||||
|
nickMethod := finalEnforcementMethod(nickAccount)
|
||||||
|
skelMethod := finalEnforcementMethod(skelAccount)
|
||||||
|
switch {
|
||||||
|
case nickMethod == NickReservationNone && skelMethod == NickReservationNone:
|
||||||
|
return "", NickReservationNone
|
||||||
|
case skelMethod == NickReservationNone:
|
||||||
|
return nickAccount, nickMethod
|
||||||
|
case nickMethod == NickReservationNone:
|
||||||
|
return skelAccount, skelMethod
|
||||||
|
default:
|
||||||
|
// nobody can use this nick
|
||||||
|
return "!", NickReservationStrict
|
||||||
|
}
|
||||||
|
} else if nickAccount == "" && skelAccount != "" {
|
||||||
|
// skeleton owner is the only owner; fall through to normal case
|
||||||
|
nickAccount = skelAccount
|
||||||
}
|
}
|
||||||
if method == NickReservationOptional {
|
// else: nickAccount != "" && skelAccount == "", nickAccount is the only owner
|
||||||
// enforcement was explicitly enabled neither in the config or by the user
|
return nickAccount, finalEnforcementMethod(nickAccount)
|
||||||
method = NickReservationNone
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Looks up the enforcement method stored in the database for an account
|
// Looks up the enforcement method stored in the database for an account
|
||||||
@ -264,10 +297,15 @@ func (am *AccountManager) AccountToClients(account string) (result []*Client) {
|
|||||||
|
|
||||||
func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
|
func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
|
||||||
casefoldedAccount, err := CasefoldName(account)
|
casefoldedAccount, err := CasefoldName(account)
|
||||||
if err != nil || account == "" || account == "*" {
|
skeleton, skerr := Skeleton(account)
|
||||||
|
if err != nil || skerr != nil || account == "" || account == "*" {
|
||||||
return errAccountCreation
|
return errAccountCreation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if restrictedNicknames[casefoldedAccount] || restrictedNicknames[skeleton] {
|
||||||
|
return errAccountAlreadyRegistered
|
||||||
|
}
|
||||||
|
|
||||||
// can't register a guest nickname
|
// can't register a guest nickname
|
||||||
config := am.server.AccountConfig()
|
config := am.server.AccountConfig()
|
||||||
renamePrefix := strings.ToLower(config.NickReservation.RenamePrefix)
|
renamePrefix := strings.ToLower(config.NickReservation.RenamePrefix)
|
||||||
@ -535,8 +573,10 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
skeleton, _ := Skeleton(raw.Name)
|
||||||
am.Lock()
|
am.Lock()
|
||||||
am.nickToAccount[casefoldedAccount] = casefoldedAccount
|
am.nickToAccount[casefoldedAccount] = casefoldedAccount
|
||||||
|
am.skeletonToAccount[skeleton] = casefoldedAccount
|
||||||
am.Unlock()
|
am.Unlock()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -567,9 +607,10 @@ func unmarshalReservedNicks(nicks string) (result []string) {
|
|||||||
|
|
||||||
func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreserve bool, reserve bool) error {
|
func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreserve bool, reserve bool) error {
|
||||||
cfnick, err := CasefoldName(nick)
|
cfnick, err := CasefoldName(nick)
|
||||||
|
skeleton, skerr := Skeleton(nick)
|
||||||
// garbage nick, or garbage options, or disabled
|
// garbage nick, or garbage options, or disabled
|
||||||
nrconfig := am.server.AccountConfig().NickReservation
|
nrconfig := am.server.AccountConfig().NickReservation
|
||||||
if err != nil || cfnick == "" || (reserve && saUnreserve) || !nrconfig.Enabled {
|
if err != nil || skerr != nil || cfnick == "" || (reserve && saUnreserve) || !nrconfig.Enabled {
|
||||||
return errAccountNickReservationFailed
|
return errAccountNickReservationFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,8 +632,15 @@ func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreser
|
|||||||
return errAccountNotLoggedIn
|
return errAccountNotLoggedIn
|
||||||
}
|
}
|
||||||
|
|
||||||
accountForNick := am.NickToAccount(cfnick)
|
am.Lock()
|
||||||
if reserve && accountForNick != "" {
|
accountForNick := am.nickToAccount[cfnick]
|
||||||
|
var accountForSkeleton string
|
||||||
|
if reserve {
|
||||||
|
accountForSkeleton = am.skeletonToAccount[skeleton]
|
||||||
|
}
|
||||||
|
am.Unlock()
|
||||||
|
|
||||||
|
if reserve && (accountForNick != "" || accountForSkeleton != "") {
|
||||||
return errNicknameReserved
|
return errNicknameReserved
|
||||||
} else if !reserve && !saUnreserve && accountForNick != account {
|
} else if !reserve && !saUnreserve && accountForNick != account {
|
||||||
return errNicknameReserved
|
return errNicknameReserved
|
||||||
@ -623,12 +671,18 @@ func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreser
|
|||||||
if len(nicks) >= nrconfig.AdditionalNickLimit {
|
if len(nicks) >= nrconfig.AdditionalNickLimit {
|
||||||
return errAccountTooManyNicks
|
return errAccountTooManyNicks
|
||||||
}
|
}
|
||||||
nicks = append(nicks, cfnick)
|
nicks = append(nicks, nick)
|
||||||
} else {
|
} else {
|
||||||
|
// compute (original reserved nicks) minus cfnick
|
||||||
var newNicks []string
|
var newNicks []string
|
||||||
for _, reservedNick := range nicks {
|
for _, reservedNick := range nicks {
|
||||||
if reservedNick != cfnick {
|
cfreservednick, _ := CasefoldName(reservedNick)
|
||||||
|
if cfreservednick != cfnick {
|
||||||
newNicks = append(newNicks, reservedNick)
|
newNicks = append(newNicks, reservedNick)
|
||||||
|
} else {
|
||||||
|
// found the original, unfolded version of the nick we're dropping;
|
||||||
|
// recompute the true skeleton from it
|
||||||
|
skeleton, _ = Skeleton(reservedNick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nicks = newNicks
|
nicks = newNicks
|
||||||
@ -650,8 +704,10 @@ func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreser
|
|||||||
defer am.Unlock()
|
defer am.Unlock()
|
||||||
if reserve {
|
if reserve {
|
||||||
am.nickToAccount[cfnick] = account
|
am.nickToAccount[cfnick] = account
|
||||||
|
am.skeletonToAccount[skeleton] = account
|
||||||
} else {
|
} else {
|
||||||
delete(am.nickToAccount, cfnick)
|
delete(am.nickToAccount, cfnick)
|
||||||
|
delete(am.skeletonToAccount, skeleton)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -787,8 +843,10 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
am.serialCacheUpdateMutex.Lock()
|
am.serialCacheUpdateMutex.Lock()
|
||||||
defer am.serialCacheUpdateMutex.Unlock()
|
defer am.serialCacheUpdateMutex.Unlock()
|
||||||
|
|
||||||
|
var accountName string
|
||||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
tx.Delete(accountKey)
|
tx.Delete(accountKey)
|
||||||
|
accountName, _ = tx.Get(accountNameKey)
|
||||||
tx.Delete(accountNameKey)
|
tx.Delete(accountNameKey)
|
||||||
tx.Delete(verifiedKey)
|
tx.Delete(verifiedKey)
|
||||||
tx.Delete(registeredTimeKey)
|
tx.Delete(registeredTimeKey)
|
||||||
@ -817,6 +875,7 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skeleton, _ := Skeleton(accountName)
|
||||||
additionalNicks := unmarshalReservedNicks(rawNicks)
|
additionalNicks := unmarshalReservedNicks(rawNicks)
|
||||||
|
|
||||||
am.Lock()
|
am.Lock()
|
||||||
@ -825,8 +884,11 @@ func (am *AccountManager) Unregister(account string) error {
|
|||||||
clients = am.accountToClients[casefoldedAccount]
|
clients = am.accountToClients[casefoldedAccount]
|
||||||
delete(am.accountToClients, casefoldedAccount)
|
delete(am.accountToClients, casefoldedAccount)
|
||||||
delete(am.nickToAccount, casefoldedAccount)
|
delete(am.nickToAccount, casefoldedAccount)
|
||||||
|
delete(am.skeletonToAccount, skeleton)
|
||||||
for _, nick := range additionalNicks {
|
for _, nick := range additionalNicks {
|
||||||
delete(am.nickToAccount, nick)
|
delete(am.nickToAccount, nick)
|
||||||
|
additionalSkel, _ := Skeleton(nick)
|
||||||
|
delete(am.skeletonToAccount, additionalSkel)
|
||||||
}
|
}
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
am.logoutOfAccount(client)
|
am.logoutOfAccount(client)
|
||||||
|
@ -95,6 +95,7 @@ type Client struct {
|
|||||||
saslMechanism string
|
saslMechanism string
|
||||||
saslValue string
|
saslValue string
|
||||||
server *Server
|
server *Server
|
||||||
|
skeleton string
|
||||||
socket *Socket
|
socket *Socket
|
||||||
stateMutex sync.RWMutex // tier 1
|
stateMutex sync.RWMutex // tier 1
|
||||||
username string
|
username string
|
||||||
@ -381,7 +382,7 @@ func (client *Client) Register() {
|
|||||||
client.TryResume()
|
client.TryResume()
|
||||||
|
|
||||||
// finish registration
|
// finish registration
|
||||||
client.updateNickMask("")
|
client.updateNickMask()
|
||||||
client.server.monitorManager.AlertAbout(client, true)
|
client.server.monitorManager.AlertAbout(client, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -565,6 +566,7 @@ func (client *Client) copyResumeData(oldClient *Client) {
|
|||||||
vhost := oldClient.vhost
|
vhost := oldClient.vhost
|
||||||
account := oldClient.account
|
account := oldClient.account
|
||||||
accountName := oldClient.accountName
|
accountName := oldClient.accountName
|
||||||
|
skeleton := oldClient.skeleton
|
||||||
oldClient.stateMutex.RUnlock()
|
oldClient.stateMutex.RUnlock()
|
||||||
|
|
||||||
// copy all flags, *except* TLS (in the case that the admins enabled
|
// copy all flags, *except* TLS (in the case that the admins enabled
|
||||||
@ -586,6 +588,7 @@ func (client *Client) copyResumeData(oldClient *Client) {
|
|||||||
client.vhost = vhost
|
client.vhost = vhost
|
||||||
client.account = account
|
client.account = account
|
||||||
client.accountName = accountName
|
client.accountName = accountName
|
||||||
|
client.skeleton = skeleton
|
||||||
client.updateNickMaskNoMutex()
|
client.updateNickMaskNoMutex()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,6 +699,14 @@ func (client *Client) Friends(capabs ...caps.Capability) ClientSet {
|
|||||||
return friends
|
return friends
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) SetOper(oper *Oper) {
|
||||||
|
client.stateMutex.Lock()
|
||||||
|
defer client.stateMutex.Unlock()
|
||||||
|
client.oper = oper
|
||||||
|
// operators typically get a vhost, update the nickmask
|
||||||
|
client.updateNickMaskNoMutex()
|
||||||
|
}
|
||||||
|
|
||||||
// XXX: CHGHOST requires prefix nickmask to have original hostname,
|
// XXX: CHGHOST requires prefix nickmask to have original hostname,
|
||||||
// this is annoying to do correctly
|
// this is annoying to do correctly
|
||||||
func (client *Client) sendChghost(oldNickMask string, vhost string) {
|
func (client *Client) sendChghost(oldNickMask string, vhost string) {
|
||||||
@ -730,32 +741,23 @@ func (client *Client) SetVHost(vhost string) (updated bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateNick updates `nick` and `nickCasefolded`.
|
// updateNick updates `nick` and `nickCasefolded`.
|
||||||
func (client *Client) updateNick(nick string) {
|
func (client *Client) updateNick(nick, nickCasefolded, skeleton string) {
|
||||||
casefoldedName, err := CasefoldName(nick)
|
|
||||||
if err != nil {
|
|
||||||
client.server.logger.Error("internal", "nick couldn't be casefolded", nick, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
|
defer client.stateMutex.Unlock()
|
||||||
client.nick = nick
|
client.nick = nick
|
||||||
client.nickCasefolded = casefoldedName
|
client.nickCasefolded = nickCasefolded
|
||||||
client.stateMutex.Unlock()
|
client.skeleton = skeleton
|
||||||
|
client.updateNickMaskNoMutex()
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateNickMask updates the casefolded nickname and nickmask.
|
// updateNickMask updates the nickmask.
|
||||||
func (client *Client) updateNickMask(nick string) {
|
func (client *Client) updateNickMask() {
|
||||||
// on "", just regenerate the nickmask etc.
|
|
||||||
// otherwise, update the actual nick
|
|
||||||
if nick != "" {
|
|
||||||
client.updateNick(nick)
|
|
||||||
}
|
|
||||||
|
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.Lock()
|
||||||
defer client.stateMutex.Unlock()
|
defer client.stateMutex.Unlock()
|
||||||
client.updateNickMaskNoMutex()
|
client.updateNickMaskNoMutex()
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateNickMask updates the casefolded nickname and nickmask, not acquiring any mutexes.
|
// updateNickMaskNoMutex updates the casefolded nickname and nickmask, not acquiring any mutexes.
|
||||||
func (client *Client) updateNickMaskNoMutex() {
|
func (client *Client) updateNickMaskNoMutex() {
|
||||||
client.hostname = client.getVHostNoMutex()
|
client.hostname = client.getVHostNoMutex()
|
||||||
if client.hostname == "" {
|
if client.hostname == "" {
|
||||||
|
@ -34,12 +34,14 @@ func ExpandUserHost(userhost string) (expanded string) {
|
|||||||
type ClientManager struct {
|
type ClientManager struct {
|
||||||
sync.RWMutex // tier 2
|
sync.RWMutex // tier 2
|
||||||
byNick map[string]*Client
|
byNick map[string]*Client
|
||||||
|
bySkeleton map[string]*Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientManager returns a new ClientManager.
|
// NewClientManager returns a new ClientManager.
|
||||||
func NewClientManager() *ClientManager {
|
func NewClientManager() *ClientManager {
|
||||||
return &ClientManager{
|
return &ClientManager{
|
||||||
byNick: make(map[string]*Client),
|
byNick: make(map[string]*Client),
|
||||||
|
bySkeleton: make(map[string]*Client),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +67,11 @@ func (clients *ClientManager) Get(nick string) *Client {
|
|||||||
|
|
||||||
func (clients *ClientManager) removeInternal(client *Client) (err error) {
|
func (clients *ClientManager) removeInternal(client *Client) (err error) {
|
||||||
// requires holding the writable Lock()
|
// requires holding the writable Lock()
|
||||||
oldcfnick := client.NickCasefolded()
|
oldcfnick, oldskeleton := client.uniqueIdentifiers()
|
||||||
|
if oldcfnick == "*" || oldcfnick == "" {
|
||||||
|
return errNickMissing
|
||||||
|
}
|
||||||
|
|
||||||
currentEntry, present := clients.byNick[oldcfnick]
|
currentEntry, present := clients.byNick[oldcfnick]
|
||||||
if present {
|
if present {
|
||||||
if currentEntry == client {
|
if currentEntry == client {
|
||||||
@ -75,7 +81,22 @@ func (clients *ClientManager) removeInternal(client *Client) (err error) {
|
|||||||
client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick)
|
client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick)
|
||||||
err = errNickMissing
|
err = errNickMissing
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
err = errNickMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentEntry, present = clients.bySkeleton[oldskeleton]
|
||||||
|
if present {
|
||||||
|
if currentEntry == client {
|
||||||
|
delete(clients.bySkeleton, oldskeleton)
|
||||||
|
} else {
|
||||||
|
client.server.logger.Warning("internal", "clients for skeleton out of sync", oldskeleton)
|
||||||
|
err = errNickMissing
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = errNickMissing
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,9 +105,6 @@ func (clients *ClientManager) Remove(client *Client) error {
|
|||||||
clients.Lock()
|
clients.Lock()
|
||||||
defer clients.Unlock()
|
defer clients.Unlock()
|
||||||
|
|
||||||
if !client.HasNick() {
|
|
||||||
return errNickMissing
|
|
||||||
}
|
|
||||||
return clients.removeInternal(client)
|
return clients.removeInternal(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +123,9 @@ func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) {
|
|||||||
}
|
}
|
||||||
// nick has been reclaimed, grant it to the new client
|
// nick has been reclaimed, grant it to the new client
|
||||||
clients.removeInternal(newClient)
|
clients.removeInternal(newClient)
|
||||||
clients.byNick[oldClient.NickCasefolded()] = newClient
|
oldcfnick, oldskeleton := oldClient.uniqueIdentifiers()
|
||||||
|
clients.byNick[oldcfnick] = newClient
|
||||||
|
clients.bySkeleton[oldskeleton] = newClient
|
||||||
|
|
||||||
newClient.copyResumeData(oldClient)
|
newClient.copyResumeData(oldClient)
|
||||||
|
|
||||||
@ -118,8 +138,12 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
newSkeleton, err := Skeleton(newNick)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick)
|
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
|
||||||
|
|
||||||
clients.Lock()
|
clients.Lock()
|
||||||
defer clients.Unlock()
|
defer clients.Unlock()
|
||||||
@ -129,12 +153,18 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
|||||||
if currentNewEntry != nil && currentNewEntry != client {
|
if currentNewEntry != nil && currentNewEntry != client {
|
||||||
return errNicknameInUse
|
return errNicknameInUse
|
||||||
}
|
}
|
||||||
|
// analogous checks for skeletons
|
||||||
|
skeletonHolder := clients.bySkeleton[newSkeleton]
|
||||||
|
if skeletonHolder != nil && skeletonHolder != client {
|
||||||
|
return errNicknameInUse
|
||||||
|
}
|
||||||
if method == NickReservationStrict && reservedAccount != "" && reservedAccount != client.Account() {
|
if method == NickReservationStrict && reservedAccount != "" && reservedAccount != client.Account() {
|
||||||
return errNicknameReserved
|
return errNicknameReserved
|
||||||
}
|
}
|
||||||
clients.removeInternal(client)
|
clients.removeInternal(client)
|
||||||
clients.byNick[newcfnick] = client
|
clients.byNick[newcfnick] = client
|
||||||
client.updateNickMask(newNick)
|
clients.bySkeleton[newSkeleton] = client
|
||||||
|
client.updateNick(newNick, newcfnick, newSkeleton)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +108,15 @@ func (client *Client) Realname() string {
|
|||||||
return client.realname
|
return client.realname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uniqueIdentifiers returns the strings for which the server enforces per-client
|
||||||
|
// uniqueness/ownership; no two clients can have colliding casefolded nicks or
|
||||||
|
// skeletons.
|
||||||
|
func (client *Client) uniqueIdentifiers() (nickCasefolded string, skeleton string) {
|
||||||
|
client.stateMutex.RLock()
|
||||||
|
defer client.stateMutex.RUnlock()
|
||||||
|
return client.nickCasefolded, client.skeleton
|
||||||
|
}
|
||||||
|
|
||||||
func (client *Client) ResumeToken() string {
|
func (client *Client) ResumeToken() string {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
@ -120,12 +129,6 @@ func (client *Client) Oper() *Oper {
|
|||||||
return client.oper
|
return client.oper
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) SetOper(oper *Oper) {
|
|
||||||
client.stateMutex.Lock()
|
|
||||||
defer client.stateMutex.Unlock()
|
|
||||||
client.oper = oper
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) Registered() bool {
|
func (client *Client) Registered() bool {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
|
@ -1706,7 +1706,6 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
|
|
||||||
oldNickmask := client.NickMaskString()
|
oldNickmask := client.NickMaskString()
|
||||||
client.SetOper(oper)
|
client.SetOper(oper)
|
||||||
client.updateNickMask("")
|
|
||||||
if client.NickMaskString() != oldNickmask {
|
if client.NickMaskString() != oldNickmask {
|
||||||
client.sendChghost(oldNickmask, oper.Vhost)
|
client.sendChghost(oldNickmask, oper.Vhost)
|
||||||
}
|
}
|
||||||
|
@ -205,9 +205,9 @@ func (nt *NickTimer) Touch() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nick := nt.client.NickCasefolded()
|
cfnick, skeleton := nt.client.uniqueIdentifiers()
|
||||||
account := nt.client.Account()
|
account := nt.client.Account()
|
||||||
accountForNick, method := nt.client.server.accounts.EnforcementStatus(nick)
|
accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton)
|
||||||
enforceTimeout := method == NickReservationWithTimeout
|
enforceTimeout := method == NickReservationWithTimeout
|
||||||
|
|
||||||
var shouldWarn bool
|
var shouldWarn bool
|
||||||
@ -223,7 +223,7 @@ func (nt *NickTimer) Touch() {
|
|||||||
// the timer will not reset as long as the squatter is targeting the same account
|
// the timer will not reset as long as the squatter is targeting the same account
|
||||||
accountChanged := accountForNick != nt.accountForNick
|
accountChanged := accountForNick != nt.accountForNick
|
||||||
// change state
|
// change state
|
||||||
nt.nick = nick
|
nt.nick = cfnick
|
||||||
nt.account = account
|
nt.account = account
|
||||||
nt.accountForNick = accountForNick
|
nt.accountForNick = accountForNick
|
||||||
delinquent := accountForNick != "" && accountForNick != account
|
delinquent := accountForNick != "" && accountForNick != account
|
||||||
|
@ -215,7 +215,7 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
nick := client.NickCasefolded()
|
nick := client.Nick()
|
||||||
err := server.accounts.SetNickReserved(client, nick, false, true)
|
err := server.accounts.SetNickReserved(client, nick, false, true)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
nsNotice(rb, fmt.Sprintf(client.t("Successfully grouped nick %s with your account"), nick))
|
nsNotice(rb, fmt.Sprintf(client.t("Successfully grouped nick %s with your account"), nick))
|
||||||
|
@ -8,21 +8,25 @@ package irc
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mtibben/confusables"
|
||||||
"golang.org/x/text/secure/precis"
|
"golang.org/x/text/secure/precis"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
casemappingName = "rfc8265"
|
casemappingName = "rfc8265"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Casefold returns a casefolded string, without doing any name or channel character checks.
|
// Each pass of PRECIS casefolding is a composition of idempotent operations,
|
||||||
func Casefold(str string) (string, error) {
|
// but not idempotent itself. Therefore, the spec says "do it four times and hope
|
||||||
var err error
|
// it converges" (lolwtf). Golang's PRECIS implementation has a "repeat" option,
|
||||||
oldStr := str
|
// which provides this functionality, but unfortunately it's not exposed publicly.
|
||||||
|
func iterateFolding(profile *precis.Profile, oldStr string) (str string, err error) {
|
||||||
|
str = oldStr
|
||||||
// follow the stabilizing rules laid out here:
|
// follow the stabilizing rules laid out here:
|
||||||
// https://tools.ietf.org/html/draft-ietf-precis-7564bis-10.html#section-7
|
// https://tools.ietf.org/html/draft-ietf-precis-7564bis-10.html#section-7
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
str, err = precis.UsernameCaseMapped.CompareKey(str)
|
str, err = profile.CompareKey(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -37,6 +41,11 @@ func Casefold(str string) (string, error) {
|
|||||||
return str, nil
|
return str, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Casefold returns a casefolded string, without doing any name or channel character checks.
|
||||||
|
func Casefold(str string) (string, error) {
|
||||||
|
return iterateFolding(precis.UsernameCaseMapped, str)
|
||||||
|
}
|
||||||
|
|
||||||
// CasefoldChannel returns a casefolded version of a channel name.
|
// CasefoldChannel returns a casefolded version of a channel name.
|
||||||
func CasefoldChannel(name string) (string, error) {
|
func CasefoldChannel(name string) (string, error) {
|
||||||
if len(name) == 0 {
|
if len(name) == 0 {
|
||||||
@ -96,3 +105,46 @@ func CasefoldName(name string) (string, error) {
|
|||||||
|
|
||||||
return lowered, err
|
return lowered, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "boring" names are exempt from skeletonization.
|
||||||
|
// this is because confusables.txt considers various pure ASCII alphanumeric
|
||||||
|
// strings confusable: 0 and O, 1 and l, m and rn. IMO this causes more problems
|
||||||
|
// than it solves.
|
||||||
|
func isBoring(name string) bool {
|
||||||
|
for i := 0; i < len(name); i += 1 {
|
||||||
|
chr := name[i]
|
||||||
|
if (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9') {
|
||||||
|
continue // alphanumerics
|
||||||
|
}
|
||||||
|
switch chr {
|
||||||
|
case '$', '%', '^', '&', '(', ')', '{', '}', '[', ']', '<', '>', '=':
|
||||||
|
continue // benign printable ascii characters
|
||||||
|
default:
|
||||||
|
return false // potentially confusable ascii like | ' `, non-ascii
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var skeletonCasefolder = precis.NewIdentifier(precis.FoldWidth, precis.LowerCase(), precis.Norm(norm.NFC))
|
||||||
|
|
||||||
|
// similar to Casefold, but exempts the bidi rule, because skeletons may
|
||||||
|
// mix scripts strangely
|
||||||
|
func casefoldSkeleton(str string) (string, error) {
|
||||||
|
return iterateFolding(skeletonCasefolder, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skeleton produces a canonicalized identifier that tries to catch
|
||||||
|
// homoglyphic / confusable identifiers. It's a tweaked version of the TR39
|
||||||
|
// skeleton algorithm. We apply the skeleton algorithm first and only then casefold,
|
||||||
|
// because casefolding first would lose some information about visual confusability.
|
||||||
|
// This has the weird consequence that the skeleton is not a function of the
|
||||||
|
// casefolded identifier --- therefore it must always be computed
|
||||||
|
// from the original (unfolded) identifier and stored/tracked separately from the
|
||||||
|
// casefolded identifier.
|
||||||
|
func Skeleton(name string) (string, error) {
|
||||||
|
if !isBoring(name) {
|
||||||
|
name = confusables.Skeleton(name)
|
||||||
|
}
|
||||||
|
return casefoldSkeleton(name)
|
||||||
|
}
|
||||||
|
@ -127,3 +127,50 @@ func TestCasefoldName(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsBoring(t *testing.T) {
|
||||||
|
assertBoring := func(str string, expected bool) {
|
||||||
|
if isBoring(str) != expected {
|
||||||
|
t.Errorf("expected [%s] to have boringness [%t], but got [%t]", str, expected, !expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertBoring("warning", true)
|
||||||
|
assertBoring("phi|ip", false)
|
||||||
|
assertBoring("Νικηφόρος", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSkeleton(t *testing.T) {
|
||||||
|
skeleton := func(str string) string {
|
||||||
|
skel, err := Skeleton(str)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
return skel
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("warning") == skeleton("waming") {
|
||||||
|
t.Errorf("Oragono shouldn't consider rn confusable with m")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("Phi|ip") != "philip" {
|
||||||
|
t.Errorf("but we still consider pipe confusable with l")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("smt") != "smt" {
|
||||||
|
t.Errorf("fullwidth characters should skeletonize to plain old ascii characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("SMT") != "smt" {
|
||||||
|
t.Errorf("after skeletonizing, we should casefold")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("еvan") != "evan" {
|
||||||
|
t.Errorf("we must protect against cyrillic homoglyph attacks")
|
||||||
|
}
|
||||||
|
|
||||||
|
if skeleton("РОТАТО") != "potato" {
|
||||||
|
t.Errorf("we must protect against cyrillic homoglyph attacks")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user