mirror of
https://github.com/ergochat/ergo.git
synced 2025-01-09 19:52:57 +01:00
commit
0e22f8d6a5
1
Makefile
1
Makefile
@ -20,6 +20,7 @@ test:
|
|||||||
python3 ./gencapdefs.py | diff - ${capdef_file}
|
python3 ./gencapdefs.py | diff - ${capdef_file}
|
||||||
cd irc && go test . && go vet .
|
cd irc && go test . && go vet .
|
||||||
cd irc/caps && go test . && go vet .
|
cd irc/caps && go test . && go vet .
|
||||||
|
cd irc/connection_limits && go test . && go vet .
|
||||||
cd irc/history && go test . && go vet .
|
cd irc/history && go test . && go vet .
|
||||||
cd irc/isupport && go test . && go vet .
|
cd irc/isupport && go test . && go vet .
|
||||||
cd irc/modes && go test . && go vet .
|
cd irc/modes && go test . && go vet .
|
||||||
|
127
irc/accounts.go
127
irc/accounts.go
@ -30,6 +30,7 @@ const (
|
|||||||
keyAccountRegTime = "account.registered.time %s"
|
keyAccountRegTime = "account.registered.time %s"
|
||||||
keyAccountCredentials = "account.credentials %s"
|
keyAccountCredentials = "account.credentials %s"
|
||||||
keyAccountAdditionalNicks = "account.additionalnicks %s"
|
keyAccountAdditionalNicks = "account.additionalnicks %s"
|
||||||
|
keyAccountEnforcement = "account.customenforcement %s"
|
||||||
keyAccountVHost = "account.vhost %s"
|
keyAccountVHost = "account.vhost %s"
|
||||||
keyCertToAccount = "account.creds.certfp %s"
|
keyCertToAccount = "account.creds.certfp %s"
|
||||||
|
|
||||||
@ -53,12 +54,14 @@ type AccountManager struct {
|
|||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
server: server,
|
server: server,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +75,8 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]string)
|
nickToAccount := make(map[string]string)
|
||||||
|
accountToMethod := make(map[string]NickReservationMethod)
|
||||||
existsPrefix := fmt.Sprintf(keyAccountExists, "")
|
existsPrefix := fmt.Sprintf(keyAccountExists, "")
|
||||||
|
|
||||||
am.serialCacheUpdateMutex.Lock()
|
am.serialCacheUpdateMutex.Lock()
|
||||||
@ -83,14 +87,22 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
|||||||
if !strings.HasPrefix(key, existsPrefix) {
|
if !strings.HasPrefix(key, existsPrefix) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
accountName := strings.TrimPrefix(key, existsPrefix)
|
|
||||||
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil {
|
account := strings.TrimPrefix(key, existsPrefix)
|
||||||
result[accountName] = accountName
|
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil {
|
||||||
|
nickToAccount[account] = account
|
||||||
}
|
}
|
||||||
if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, accountName)); 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 {
|
||||||
result[nick] = accountName
|
nickToAccount[nick] = account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil {
|
||||||
|
method, err := nickReservationFromString(methodStr)
|
||||||
|
if err == nil {
|
||||||
|
accountToMethod[account] = method
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -99,10 +111,11 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", fmt.Sprintf("couldn't read reserved nicks: %v", err))
|
am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error())
|
||||||
} else {
|
} else {
|
||||||
am.Lock()
|
am.Lock()
|
||||||
am.nickToAccount = result
|
am.nickToAccount = nickToAccount
|
||||||
|
am.accountToMethod = accountToMethod
|
||||||
am.Unlock()
|
am.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,6 +169,84 @@ func (am *AccountManager) NickToAccount(nick string) string {
|
|||||||
return am.nickToAccount[cfnick]
|
return am.nickToAccount[cfnick]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
|
||||||
|
// used to enforce ownership.
|
||||||
|
func (am *AccountManager) EnforcementStatus(nick string) (account string, method NickReservationMethod) {
|
||||||
|
cfnick, err := CasefoldName(nick)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := am.server.Config()
|
||||||
|
if !config.Accounts.NickReservation.Enabled {
|
||||||
|
method = NickReservationNone
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
am.RLock()
|
||||||
|
defer am.RUnlock()
|
||||||
|
|
||||||
|
account = am.nickToAccount[cfnick]
|
||||||
|
method = am.accountToMethod[account]
|
||||||
|
// if they don't have a custom setting, or customization is disabled, use the default
|
||||||
|
if method == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
|
||||||
|
method = config.Accounts.NickReservation.Method
|
||||||
|
}
|
||||||
|
if method == NickReservationOptional {
|
||||||
|
// enforcement was explicitly enabled neither in the config or by the user
|
||||||
|
method = NickReservationNone
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks up the enforcement method stored in the database for an account
|
||||||
|
// (typically you want EnforcementStatus instead, which respects the config)
|
||||||
|
func (am *AccountManager) getStoredEnforcementStatus(account string) string {
|
||||||
|
am.RLock()
|
||||||
|
defer am.RUnlock()
|
||||||
|
return nickReservationToString(am.accountToMethod[account])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets a custom enforcement method for an account and stores it in the database.
|
||||||
|
func (am *AccountManager) SetEnforcementStatus(account string, method NickReservationMethod) (err error) {
|
||||||
|
config := am.server.Config()
|
||||||
|
if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
|
||||||
|
return errFeatureDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
var serialized string
|
||||||
|
if method == NickReservationOptional {
|
||||||
|
serialized = "" // normally this is "default", but we're going to delete the key
|
||||||
|
} else {
|
||||||
|
serialized = nickReservationToString(method)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := fmt.Sprintf(keyAccountEnforcement, account)
|
||||||
|
|
||||||
|
am.Lock()
|
||||||
|
defer am.Unlock()
|
||||||
|
|
||||||
|
currentMethod := am.accountToMethod[account]
|
||||||
|
if method != currentMethod {
|
||||||
|
if method == NickReservationOptional {
|
||||||
|
delete(am.accountToMethod, account)
|
||||||
|
} else {
|
||||||
|
am.accountToMethod[account] = method
|
||||||
|
}
|
||||||
|
|
||||||
|
return am.server.store.Update(func(tx *buntdb.Tx) (err error) {
|
||||||
|
if serialized != "" {
|
||||||
|
_, _, err = tx.Set(key, nickReservationToString(method), nil)
|
||||||
|
} else {
|
||||||
|
_, err = tx.Delete(key)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (am *AccountManager) AccountToClients(account string) (result []*Client) {
|
func (am *AccountManager) AccountToClients(account string) (result []*Client) {
|
||||||
cfaccount, err := CasefoldName(account)
|
cfaccount, err := CasefoldName(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -286,14 +377,14 @@ func (am *AccountManager) serializeCredentials(passphrase string, certfp string)
|
|||||||
bcryptCost := int(am.server.Config().Accounts.Registration.BcryptCost)
|
bcryptCost := int(am.server.Config().Accounts.Registration.BcryptCost)
|
||||||
creds.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost)
|
creds.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
|
am.server.logger.Error("internal", "could not hash password", err.Error())
|
||||||
return "", errAccountCreation
|
return "", errAccountCreation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
credText, err := json.Marshal(creds)
|
credText, err := json.Marshal(creds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err))
|
am.server.logger.Error("internal", "could not marshal credentials", err.Error())
|
||||||
return "", errAccountCreation
|
return "", errAccountCreation
|
||||||
}
|
}
|
||||||
return string(credText), nil
|
return string(credText), nil
|
||||||
@ -367,7 +458,7 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou
|
|||||||
// config.TLS.InsecureSkipVerify
|
// config.TLS.InsecureSkipVerify
|
||||||
err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
|
err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", fmt.Sprintf("Failed to dispatch e-mail: %v", err))
|
am.server.logger.Error("internal", "Failed to dispatch e-mail", err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -576,7 +667,9 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
|
|||||||
case 0:
|
case 0:
|
||||||
err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase)
|
err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase)
|
||||||
case 1:
|
case 1:
|
||||||
err = passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase))
|
if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil {
|
||||||
|
err = errAccountInvalidCredentials
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = errAccountInvalidCredentials
|
err = errAccountInvalidCredentials
|
||||||
}
|
}
|
||||||
@ -619,7 +712,7 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl
|
|||||||
result.RegisteredAt = time.Unix(regTimeInt, 0)
|
result.RegisteredAt = time.Unix(regTimeInt, 0)
|
||||||
e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
|
e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
am.server.logger.Error("internal", fmt.Sprintf("could not unmarshal credentials: %v", e))
|
am.server.logger.Error("internal", "could not unmarshal credentials", e.Error())
|
||||||
err = errAccountDoesNotExist
|
err = errAccountDoesNotExist
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -628,7 +721,7 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl
|
|||||||
if raw.VHost != "" {
|
if raw.VHost != "" {
|
||||||
e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
|
e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
am.server.logger.Warning("internal", fmt.Sprintf("could not unmarshal vhost for account %s: %v", result.Name, e))
|
am.server.logger.Warning("internal", "could not unmarshal vhost for account", result.Name, e.Error())
|
||||||
// pretend they have no vhost and move on
|
// pretend they have no vhost and move on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -992,10 +1085,12 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo)
|
|||||||
|
|
||||||
func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
||||||
changed := client.SetAccountName(account.Name)
|
changed := client.SetAccountName(account.Name)
|
||||||
if changed {
|
if !changed {
|
||||||
go client.nickTimer.Touch()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.nickTimer.Touch()
|
||||||
|
|
||||||
am.applyVHostInfo(client, account.VHost)
|
am.applyVHostInfo(client, account.VHost)
|
||||||
|
|
||||||
casefoldedAccount := client.Account()
|
casefoldedAccount := client.Account()
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sync"
|
"sync"
|
||||||
@ -48,7 +49,7 @@ type Channel struct {
|
|||||||
func NewChannel(s *Server, name string, regInfo *RegisteredChannel) *Channel {
|
func NewChannel(s *Server, name string, regInfo *RegisteredChannel) *Channel {
|
||||||
casefoldedName, err := CasefoldChannel(name)
|
casefoldedName, err := CasefoldChannel(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("internal", fmt.Sprintf("Bad channel name %s: %v", name, err))
|
s.logger.Error("internal", "Bad channel name", name, err.Error())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,8 +347,7 @@ func (channel *Channel) IsEmpty() bool {
|
|||||||
|
|
||||||
// Join joins the given client to this channel (if they can be joined).
|
// Join joins the given client to this channel (if they can be joined).
|
||||||
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
|
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
|
||||||
account := client.Account()
|
details := client.Details()
|
||||||
nickMaskCasefolded := client.NickMaskCasefolded()
|
|
||||||
|
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
chname := channel.name
|
chname := channel.name
|
||||||
@ -357,7 +357,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
limit := channel.userLimit
|
limit := channel.userLimit
|
||||||
chcount := len(channel.members)
|
chcount := len(channel.members)
|
||||||
_, alreadyJoined := channel.members[client]
|
_, alreadyJoined := channel.members[client]
|
||||||
persistentMode := channel.accountToUMode[account]
|
persistentMode := channel.accountToUMode[details.account]
|
||||||
channel.stateMutex.RUnlock()
|
channel.stateMutex.RUnlock()
|
||||||
|
|
||||||
if alreadyJoined {
|
if alreadyJoined {
|
||||||
@ -367,7 +367,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
|
|
||||||
// the founder can always join (even if they disabled auto +q on join);
|
// the founder can always join (even if they disabled auto +q on join);
|
||||||
// anyone who automatically receives halfop or higher can always join
|
// anyone who automatically receives halfop or higher can always join
|
||||||
hasPrivs := isSajoin || (founder != "" && founder == account) || (persistentMode != 0 && persistentMode != modes.Voice)
|
hasPrivs := isSajoin || (founder != "" && founder == details.account) || (persistentMode != 0 && persistentMode != modes.Voice)
|
||||||
|
|
||||||
if !hasPrivs && limit != 0 && chcount >= limit {
|
if !hasPrivs && limit != 0 && chcount >= limit {
|
||||||
rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
|
rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
|
||||||
@ -379,20 +379,20 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isInvited := client.CheckInvited(chcfname) || channel.lists[modes.InviteMask].Match(nickMaskCasefolded)
|
isInvited := client.CheckInvited(chcfname) || channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded)
|
||||||
if !hasPrivs && channel.flags.HasMode(modes.InviteOnly) && !isInvited {
|
if !hasPrivs && channel.flags.HasMode(modes.InviteOnly) && !isInvited {
|
||||||
rb.Add(nil, client.server.name, ERR_INVITEONLYCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "i"))
|
rb.Add(nil, client.server.name, ERR_INVITEONLYCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "i"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasPrivs && channel.lists[modes.BanMask].Match(nickMaskCasefolded) &&
|
if !hasPrivs && channel.lists[modes.BanMask].Match(details.nickMaskCasefolded) &&
|
||||||
!isInvited &&
|
!isInvited &&
|
||||||
!channel.lists[modes.ExceptMask].Match(nickMaskCasefolded) {
|
!channel.lists[modes.ExceptMask].Match(details.nickMaskCasefolded) {
|
||||||
rb.Add(nil, client.server.name, ERR_BANNEDFROMCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "b"))
|
rb.Add(nil, client.server.name, ERR_BANNEDFROMCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "b"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", client.nick, chname))
|
client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
|
||||||
|
|
||||||
newChannel, givenMode := func() (newChannel bool, givenMode modes.Mode) {
|
newChannel, givenMode := func() (newChannel bool, givenMode modes.Mode) {
|
||||||
channel.joinPartMutex.Lock()
|
channel.joinPartMutex.Lock()
|
||||||
@ -416,15 +416,19 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
channel.regenerateMembersCache()
|
channel.regenerateMembersCache()
|
||||||
|
|
||||||
|
channel.history.Add(history.Item{
|
||||||
|
Type: history.Join,
|
||||||
|
Nick: details.nickMask,
|
||||||
|
AccountName: details.accountName,
|
||||||
|
Msgid: details.realname,
|
||||||
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}()
|
}()
|
||||||
|
|
||||||
client.addChannel(channel)
|
client.addChannel(channel)
|
||||||
|
|
||||||
nick := client.Nick()
|
|
||||||
nickmask := client.NickMaskString()
|
|
||||||
realname := client.Realname()
|
|
||||||
accountName := client.AccountName()
|
|
||||||
var modestr string
|
var modestr string
|
||||||
if givenMode != 0 {
|
if givenMode != 0 {
|
||||||
modestr = fmt.Sprintf("+%v", givenMode)
|
modestr = fmt.Sprintf("+%v", givenMode)
|
||||||
@ -435,19 +439,19 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if member.capabilities.Has(caps.ExtendedJoin) {
|
if member.capabilities.Has(caps.ExtendedJoin) {
|
||||||
member.Send(nil, nickmask, "JOIN", chname, accountName, realname)
|
member.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
||||||
} else {
|
} else {
|
||||||
member.Send(nil, nickmask, "JOIN", chname)
|
member.Send(nil, details.nickMask, "JOIN", chname)
|
||||||
}
|
}
|
||||||
if givenMode != 0 {
|
if givenMode != 0 {
|
||||||
member.Send(nil, client.server.name, "MODE", chname, modestr, nick)
|
member.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.capabilities.Has(caps.ExtendedJoin) {
|
if client.capabilities.Has(caps.ExtendedJoin) {
|
||||||
rb.Add(nil, nickmask, "JOIN", chname, accountName, realname)
|
rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, nickmask, "JOIN", chname)
|
rb.Add(nil, details.nickMask, "JOIN", chname)
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't send topic when it's an entirely new channel
|
// don't send topic when it's an entirely new channel
|
||||||
@ -458,16 +462,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
channel.Names(client, rb)
|
channel.Names(client, rb)
|
||||||
|
|
||||||
if givenMode != 0 {
|
if givenMode != 0 {
|
||||||
rb.Add(nil, client.server.name, "MODE", chname, modestr, nick)
|
rb.Add(nil, client.server.name, "MODE", chname, modestr, details.nick)
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.history.Add(history.Item{
|
|
||||||
Type: history.Join,
|
|
||||||
Nick: nickmask,
|
|
||||||
AccountName: accountName,
|
|
||||||
Msgid: realname,
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
|
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
|
||||||
rb.Flush(true)
|
rb.Flush(true)
|
||||||
|
|
||||||
@ -489,20 +486,20 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
|
|||||||
|
|
||||||
channel.Quit(client)
|
channel.Quit(client)
|
||||||
|
|
||||||
nickmask := client.NickMaskString()
|
details := client.Details()
|
||||||
for _, member := range channel.Members() {
|
for _, member := range channel.Members() {
|
||||||
member.Send(nil, nickmask, "PART", chname, message)
|
member.Send(nil, details.nickMask, "PART", chname, message)
|
||||||
}
|
}
|
||||||
rb.Add(nil, nickmask, "PART", chname, message)
|
rb.Add(nil, details.nickMask, "PART", chname, message)
|
||||||
|
|
||||||
channel.history.Add(history.Item{
|
channel.history.Add(history.Item{
|
||||||
Type: history.Part,
|
Type: history.Part,
|
||||||
Nick: nickmask,
|
Nick: details.nickMask,
|
||||||
AccountName: client.AccountName(),
|
AccountName: details.accountName,
|
||||||
Message: utils.MakeSplitMessage(message, true),
|
Message: utils.MakeSplitMessage(message, true),
|
||||||
})
|
})
|
||||||
|
|
||||||
client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", client.nick, chname))
|
client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume is called after a successful global resume to:
|
// Resume is called after a successful global resume to:
|
||||||
@ -591,10 +588,17 @@ func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Tim
|
|||||||
rb.Send(true)
|
rb.Send(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stripMaskFromNick(nickMask string) (nick string) {
|
||||||
|
index := strings.Index(nickMask, "!")
|
||||||
|
if index == -1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return nickMask[0:index]
|
||||||
|
}
|
||||||
|
|
||||||
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
|
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
|
||||||
chname := channel.Name()
|
chname := channel.Name()
|
||||||
client := rb.target
|
client := rb.target
|
||||||
extendedJoin := client.capabilities.Has(caps.ExtendedJoin)
|
|
||||||
serverTime := client.capabilities.Has(caps.ServerTime)
|
serverTime := client.capabilities.Has(caps.ServerTime)
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
@ -609,21 +613,27 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
|
|||||||
case history.Notice:
|
case history.Notice:
|
||||||
rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message)
|
rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message)
|
||||||
case history.Join:
|
case history.Join:
|
||||||
if extendedJoin {
|
nick := stripMaskFromNick(item.Nick)
|
||||||
// XXX Msgid is the realname in this case
|
var message string
|
||||||
rb.Add(tags, item.Nick, "JOIN", chname, item.AccountName, item.Msgid)
|
if item.AccountName == "*" {
|
||||||
|
message = fmt.Sprintf(client.t("%s joined the channel"), nick)
|
||||||
} else {
|
} else {
|
||||||
rb.Add(tags, item.Nick, "JOIN", chname)
|
message = fmt.Sprintf(client.t("%s [account: %s] joined the channel"), nick, item.AccountName)
|
||||||
}
|
}
|
||||||
case history.Quit:
|
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
|
||||||
// XXX: send QUIT as PART to avoid having to correctly deduplicate and synchronize
|
|
||||||
// QUIT messages across channels
|
|
||||||
fallthrough
|
|
||||||
case history.Part:
|
case history.Part:
|
||||||
rb.Add(tags, item.Nick, "PART", chname, item.Message.Original)
|
nick := stripMaskFromNick(item.Nick)
|
||||||
|
message := fmt.Sprintf(client.t("%s left the channel (%s)"), nick, item.Message.Original)
|
||||||
|
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
|
||||||
|
case history.Quit:
|
||||||
|
nick := stripMaskFromNick(item.Nick)
|
||||||
|
message := fmt.Sprintf(client.t("%s quit (%s)"), nick, item.Message.Original)
|
||||||
|
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
|
||||||
case history.Kick:
|
case history.Kick:
|
||||||
|
nick := stripMaskFromNick(item.Nick)
|
||||||
// XXX Msgid is the kick target
|
// XXX Msgid is the kick target
|
||||||
rb.Add(tags, item.Nick, "KICK", chname, item.Msgid, item.Message.Original)
|
message := fmt.Sprintf(client.t("%s kicked %s (%s)"), nick, item.Msgid, item.Message.Original)
|
||||||
|
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/goshuirc/irc-go/ircmsg"
|
"github.com/goshuirc/irc-go/ircmsg"
|
||||||
ident "github.com/oragono/go-ident"
|
ident "github.com/oragono/go-ident"
|
||||||
"github.com/oragono/oragono/irc/caps"
|
"github.com/oragono/oragono/irc/caps"
|
||||||
|
"github.com/oragono/oragono/irc/connection_limits"
|
||||||
"github.com/oragono/oragono/irc/history"
|
"github.com/oragono/oragono/irc/history"
|
||||||
"github.com/oragono/oragono/irc/modes"
|
"github.com/oragono/oragono/irc/modes"
|
||||||
"github.com/oragono/oragono/irc/sno"
|
"github.com/oragono/oragono/irc/sno"
|
||||||
@ -52,7 +53,7 @@ type ResumeDetails struct {
|
|||||||
// Client is an IRC client.
|
// Client is an IRC client.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
account string
|
account string
|
||||||
accountName string
|
accountName string // display name of the account: uncasefolded, '*' if not logged in
|
||||||
atime time.Time
|
atime time.Time
|
||||||
authorized bool
|
authorized bool
|
||||||
awayMessage string
|
awayMessage string
|
||||||
@ -73,6 +74,7 @@ type Client struct {
|
|||||||
isDestroyed bool
|
isDestroyed bool
|
||||||
isQuitting bool
|
isQuitting bool
|
||||||
languages []string
|
languages []string
|
||||||
|
loginThrottle connection_limits.GenericThrottle
|
||||||
maxlenTags uint32
|
maxlenTags uint32
|
||||||
maxlenRest uint32
|
maxlenRest uint32
|
||||||
nick string
|
nick string
|
||||||
@ -100,6 +102,25 @@ type Client struct {
|
|||||||
history *history.Buffer
|
history *history.Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WhoWas is the subset of client details needed to answer a WHOWAS query
|
||||||
|
type WhoWas struct {
|
||||||
|
nick string
|
||||||
|
nickCasefolded string
|
||||||
|
username string
|
||||||
|
hostname string
|
||||||
|
realname string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientDetails is a standard set of details about a client
|
||||||
|
type ClientDetails struct {
|
||||||
|
WhoWas
|
||||||
|
|
||||||
|
nickMask string
|
||||||
|
nickMaskCasefolded string
|
||||||
|
account string
|
||||||
|
accountName string
|
||||||
|
}
|
||||||
|
|
||||||
// NewClient sets up a new client and starts its goroutine.
|
// NewClient sets up a new client and starts its goroutine.
|
||||||
func NewClient(server *Server, conn net.Conn, isTLS bool) {
|
func NewClient(server *Server, conn net.Conn, isTLS bool) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@ -115,8 +136,13 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) {
|
|||||||
channels: make(ChannelSet),
|
channels: make(ChannelSet),
|
||||||
ctime: now,
|
ctime: now,
|
||||||
flags: modes.NewModeSet(),
|
flags: modes.NewModeSet(),
|
||||||
|
loginThrottle: connection_limits.GenericThrottle{
|
||||||
|
Duration: config.Accounts.LoginThrottling.Duration,
|
||||||
|
Limit: config.Accounts.LoginThrottling.MaxAttempts,
|
||||||
|
},
|
||||||
server: server,
|
server: server,
|
||||||
socket: socket,
|
socket: socket,
|
||||||
|
accountName: "*",
|
||||||
nick: "*", // * is used until actual nick is given
|
nick: "*", // * is used until actual nick is given
|
||||||
nickCasefolded: "*",
|
nickCasefolded: "*",
|
||||||
nickMaskString: "*", // * is used until actual nick is given
|
nickMaskString: "*", // * is used until actual nick is given
|
||||||
|
@ -72,7 +72,7 @@ func (clients *ClientManager) removeInternal(client *Client) (err error) {
|
|||||||
delete(clients.byNick, oldcfnick)
|
delete(clients.byNick, oldcfnick)
|
||||||
} else {
|
} else {
|
||||||
// this shouldn't happen, but we can ignore it
|
// this shouldn't happen, but we can ignore it
|
||||||
client.server.logger.Warning("internal", fmt.Sprintf("clients for nick %s out of sync", oldcfnick))
|
client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick)
|
||||||
err = errNickMissing
|
err = errNickMissing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,17 +119,11 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var reservedAccount string
|
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick)
|
||||||
var method NickReservationMethod
|
|
||||||
if client.server.AccountConfig().NickReservation.Enabled {
|
|
||||||
reservedAccount = client.server.accounts.NickToAccount(newcfnick)
|
|
||||||
method = client.server.AccountConfig().NickReservation.Method
|
|
||||||
}
|
|
||||||
|
|
||||||
clients.Lock()
|
clients.Lock()
|
||||||
defer clients.Unlock()
|
defer clients.Unlock()
|
||||||
|
|
||||||
clients.removeInternal(client)
|
|
||||||
currentNewEntry := clients.byNick[newcfnick]
|
currentNewEntry := clients.byNick[newcfnick]
|
||||||
// the client may just be changing case
|
// the client may just be changing case
|
||||||
if currentNewEntry != nil && currentNewEntry != client {
|
if currentNewEntry != nil && currentNewEntry != client {
|
||||||
@ -138,6 +132,7 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
|||||||
if method == NickReservationStrict && reservedAccount != client.Account() {
|
if method == NickReservationStrict && reservedAccount != client.Account() {
|
||||||
return errNicknameReserved
|
return errNicknameReserved
|
||||||
}
|
}
|
||||||
|
clients.removeInternal(client)
|
||||||
clients.byNick[newcfnick] = client
|
clients.byNick[newcfnick] = client
|
||||||
client.updateNickMask(newNick)
|
client.updateNickMask(newNick)
|
||||||
return nil
|
return nil
|
||||||
|
@ -8,7 +8,6 @@ package irc
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
@ -55,6 +54,11 @@ func (conf *TLSListenConfig) Config() (*tls.Config, error) {
|
|||||||
type AccountConfig struct {
|
type AccountConfig struct {
|
||||||
Registration AccountRegistrationConfig
|
Registration AccountRegistrationConfig
|
||||||
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
||||||
|
LoginThrottling struct {
|
||||||
|
Enabled bool
|
||||||
|
Duration time.Duration
|
||||||
|
MaxAttempts int `yaml:"max-attempts"`
|
||||||
|
} `yaml:"login-throttling"`
|
||||||
SkipServerPassword bool `yaml:"skip-server-password"`
|
SkipServerPassword bool `yaml:"skip-server-password"`
|
||||||
NickReservation NickReservationConfig `yaml:"nick-reservation"`
|
NickReservation NickReservationConfig `yaml:"nick-reservation"`
|
||||||
VHosts VHostConfig
|
VHosts VHostConfig
|
||||||
@ -100,10 +104,50 @@ type VHostConfig struct {
|
|||||||
type NickReservationMethod int
|
type NickReservationMethod int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NickReservationWithTimeout NickReservationMethod = iota
|
// NickReservationOptional is the zero value; it serializes to
|
||||||
|
// "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`.
|
||||||
|
// in both cases, it means "defer to the other source of truth", i.e.,
|
||||||
|
// in the config, defer to the user's custom setting, and as a custom setting,
|
||||||
|
// defer to the default in the config. if both are NickReservationOptional then
|
||||||
|
// there is no enforcement.
|
||||||
|
NickReservationOptional NickReservationMethod = iota
|
||||||
|
NickReservationNone
|
||||||
|
NickReservationWithTimeout
|
||||||
NickReservationStrict
|
NickReservationStrict
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func nickReservationToString(method NickReservationMethod) string {
|
||||||
|
switch method {
|
||||||
|
case NickReservationOptional:
|
||||||
|
return "default"
|
||||||
|
case NickReservationNone:
|
||||||
|
return "none"
|
||||||
|
case NickReservationWithTimeout:
|
||||||
|
return "timeout"
|
||||||
|
case NickReservationStrict:
|
||||||
|
return "strict"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nickReservationFromString(method string) (NickReservationMethod, error) {
|
||||||
|
switch method {
|
||||||
|
case "default":
|
||||||
|
return NickReservationOptional, nil
|
||||||
|
case "optional":
|
||||||
|
return NickReservationOptional, nil
|
||||||
|
case "none":
|
||||||
|
return NickReservationNone, nil
|
||||||
|
case "timeout":
|
||||||
|
return NickReservationWithTimeout, nil
|
||||||
|
case "strict":
|
||||||
|
return NickReservationStrict, nil
|
||||||
|
default:
|
||||||
|
return NickReservationOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
var orig, raw string
|
var orig, raw string
|
||||||
var err error
|
var err error
|
||||||
@ -113,20 +157,18 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
|
|||||||
if raw, err = Casefold(orig); err != nil {
|
if raw, err = Casefold(orig); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if raw == "timeout" {
|
method, err := nickReservationFromString(raw)
|
||||||
*nr = NickReservationWithTimeout
|
if err == nil {
|
||||||
} else if raw == "strict" {
|
*nr = method
|
||||||
*nr = NickReservationStrict
|
|
||||||
} else {
|
|
||||||
return errors.New(fmt.Sprintf("invalid nick-reservation.method value: %s", orig))
|
|
||||||
}
|
}
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type NickReservationConfig struct {
|
type NickReservationConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
AdditionalNickLimit int `yaml:"additional-nick-limit"`
|
AdditionalNickLimit int `yaml:"additional-nick-limit"`
|
||||||
Method NickReservationMethod
|
Method NickReservationMethod
|
||||||
|
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
|
||||||
RenameTimeout time.Duration `yaml:"rename-timeout"`
|
RenameTimeout time.Duration `yaml:"rename-timeout"`
|
||||||
RenamePrefix string `yaml:"rename-prefix"`
|
RenamePrefix string `yaml:"rename-prefix"`
|
||||||
}
|
}
|
||||||
@ -558,6 +600,10 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
|
config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !config.Accounts.LoginThrottling.Enabled {
|
||||||
|
config.Accounts.LoginThrottling.MaxAttempts = 0 // limit of 0 means disabled
|
||||||
|
}
|
||||||
|
|
||||||
maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
|
maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())
|
return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())
|
||||||
|
@ -27,7 +27,44 @@ type ThrottlerConfig struct {
|
|||||||
// ThrottleDetails holds the connection-throttling details for a subnet/IP.
|
// ThrottleDetails holds the connection-throttling details for a subnet/IP.
|
||||||
type ThrottleDetails struct {
|
type ThrottleDetails struct {
|
||||||
Start time.Time
|
Start time.Time
|
||||||
ClientCount int
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenericThrottle allows enforcing limits of the form
|
||||||
|
// "at most X events per time window of duration Y"
|
||||||
|
type GenericThrottle struct {
|
||||||
|
ThrottleDetails // variable state: what events have been seen
|
||||||
|
// these are constant after creation:
|
||||||
|
Duration time.Duration // window length to consider
|
||||||
|
Limit int // number of events allowed per window
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch checks whether an additional event is allowed:
|
||||||
|
// it either denies it (by returning false) or allows it (by returning true)
|
||||||
|
// and records it
|
||||||
|
func (g *GenericThrottle) Touch() (throttled bool, remainingTime time.Duration) {
|
||||||
|
return g.touch(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GenericThrottle) touch(now time.Time) (throttled bool, remainingTime time.Duration) {
|
||||||
|
if g.Limit == 0 {
|
||||||
|
return // limit of 0 disables throttling
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := now.Sub(g.Start)
|
||||||
|
if elapsed > g.Duration {
|
||||||
|
// reset window, record the operation
|
||||||
|
g.Start = now
|
||||||
|
g.Count = 1
|
||||||
|
return false, 0
|
||||||
|
} else if g.Count >= g.Limit {
|
||||||
|
// we are throttled
|
||||||
|
return true, g.Start.Add(g.Duration).Sub(now)
|
||||||
|
} else {
|
||||||
|
// we are not throttled, record the operation
|
||||||
|
g.Count += 1
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throttler manages automated client connection throttling.
|
// Throttler manages automated client connection throttling.
|
||||||
@ -102,21 +139,21 @@ func (ct *Throttler) AddClient(addr net.IP) error {
|
|||||||
ct.maskAddr(addr)
|
ct.maskAddr(addr)
|
||||||
addrString := addr.String()
|
addrString := addr.String()
|
||||||
|
|
||||||
details, exists := ct.population[addrString]
|
details := ct.population[addrString] // retrieve mutable throttle state from the map
|
||||||
if !exists || details.Start.Add(ct.duration).Before(time.Now()) {
|
// add in constant state to process the limiting operation
|
||||||
details = ThrottleDetails{
|
g := GenericThrottle{
|
||||||
Start: time.Now(),
|
ThrottleDetails: details,
|
||||||
}
|
Duration: ct.duration,
|
||||||
|
Limit: ct.subnetLimit,
|
||||||
}
|
}
|
||||||
|
throttled, _ := g.Touch() // actually check the limit
|
||||||
|
ct.population[addrString] = g.ThrottleDetails // store modified mutable state
|
||||||
|
|
||||||
if details.ClientCount+1 > ct.subnetLimit {
|
if throttled {
|
||||||
return errTooManyClients
|
return errTooManyClients
|
||||||
}
|
} else {
|
||||||
|
|
||||||
details.ClientCount++
|
|
||||||
ct.population[addrString] = details
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ct *Throttler) BanDuration() time.Duration {
|
func (ct *Throttler) BanDuration() time.Duration {
|
||||||
|
86
irc/connection_limits/throttler_test.go
Normal file
86
irc/connection_limits/throttler_test.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) 2018 Shivaram Lingamneni
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package connection_limits
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assertEqual(supplied, expected interface{}, t *testing.T) {
|
||||||
|
if !reflect.DeepEqual(supplied, expected) {
|
||||||
|
t.Errorf("expected %v but got %v", expected, supplied)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenericThrottle(t *testing.T) {
|
||||||
|
minute, _ := time.ParseDuration("1m")
|
||||||
|
second, _ := time.ParseDuration("1s")
|
||||||
|
zero, _ := time.ParseDuration("0s")
|
||||||
|
|
||||||
|
throttler := GenericThrottle{
|
||||||
|
Duration: minute,
|
||||||
|
Limit: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
throttled, remaining := throttler.touch(now)
|
||||||
|
assertEqual(throttled, false, t)
|
||||||
|
assertEqual(remaining, zero, t)
|
||||||
|
|
||||||
|
now = now.Add(second)
|
||||||
|
throttled, remaining = throttler.touch(now)
|
||||||
|
assertEqual(throttled, false, t)
|
||||||
|
assertEqual(remaining, zero, t)
|
||||||
|
|
||||||
|
now = now.Add(second)
|
||||||
|
throttled, remaining = throttler.touch(now)
|
||||||
|
assertEqual(throttled, true, t)
|
||||||
|
assertEqual(remaining, 58*second, t)
|
||||||
|
|
||||||
|
now = now.Add(minute)
|
||||||
|
throttled, remaining = throttler.touch(now)
|
||||||
|
assertEqual(throttled, false, t)
|
||||||
|
assertEqual(remaining, zero, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenericThrottleDisabled(t *testing.T) {
|
||||||
|
minute, _ := time.ParseDuration("1m")
|
||||||
|
throttler := GenericThrottle{
|
||||||
|
Duration: minute,
|
||||||
|
Limit: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 1024; i += 1 {
|
||||||
|
throttled, _ := throttler.Touch()
|
||||||
|
if throttled {
|
||||||
|
t.Error("disabled throttler should not throttle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectionThrottle(t *testing.T) {
|
||||||
|
minute, _ := time.ParseDuration("1m")
|
||||||
|
maxConnections := 3
|
||||||
|
config := ThrottlerConfig{
|
||||||
|
Enabled: true,
|
||||||
|
CidrLenIPv4: 32,
|
||||||
|
CidrLenIPv6: 64,
|
||||||
|
ConnectionsPerCidr: maxConnections,
|
||||||
|
Duration: minute,
|
||||||
|
}
|
||||||
|
throttler := NewThrottler()
|
||||||
|
throttler.ApplyConfig(config)
|
||||||
|
|
||||||
|
addr := net.ParseIP("8.8.8.8")
|
||||||
|
|
||||||
|
for i := 0; i < maxConnections; i += 1 {
|
||||||
|
err := throttler.AddClient(addr)
|
||||||
|
assertEqual(err, nil, t)
|
||||||
|
}
|
||||||
|
err := throttler.AddClient(addr)
|
||||||
|
assertEqual(err, errTooManyClients, t)
|
||||||
|
}
|
@ -40,6 +40,7 @@ var (
|
|||||||
errSaslFail = errors.New("SASL failed")
|
errSaslFail = errors.New("SASL failed")
|
||||||
errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token")
|
errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token")
|
||||||
errInvalidUsername = errors.New("Invalid username")
|
errInvalidUsername = errors.New("Invalid username")
|
||||||
|
errFeatureDisabled = errors.New("That feature is disabled")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Socket Errors
|
// Socket Errors
|
||||||
|
@ -147,9 +147,6 @@ func (client *Client) Account() string {
|
|||||||
func (client *Client) AccountName() string {
|
func (client *Client) AccountName() string {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
if client.accountName == "" {
|
|
||||||
return "*"
|
|
||||||
}
|
|
||||||
return client.accountName
|
return client.accountName
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,18 +179,6 @@ func (client *Client) SetAuthorized(authorized bool) {
|
|||||||
client.authorized = authorized
|
client.authorized = authorized
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) PreregNick() string {
|
|
||||||
client.stateMutex.RLock()
|
|
||||||
defer client.stateMutex.RUnlock()
|
|
||||||
return client.preregNick
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) SetPreregNick(preregNick string) {
|
|
||||||
client.stateMutex.Lock()
|
|
||||||
defer client.stateMutex.Unlock()
|
|
||||||
client.preregNick = preregNick
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) HasMode(mode modes.Mode) bool {
|
func (client *Client) HasMode(mode modes.Mode) bool {
|
||||||
// client.flags has its own synch
|
// client.flags has its own synch
|
||||||
return client.flags.HasMode(mode)
|
return client.flags.HasMode(mode)
|
||||||
@ -217,15 +202,22 @@ func (client *Client) Channels() (result []*Channel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) WhoWas() (result WhoWas) {
|
func (client *Client) WhoWas() (result WhoWas) {
|
||||||
|
return client.Details().WhoWas
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Details() (result ClientDetails) {
|
||||||
client.stateMutex.RLock()
|
client.stateMutex.RLock()
|
||||||
defer client.stateMutex.RUnlock()
|
defer client.stateMutex.RUnlock()
|
||||||
|
|
||||||
result.nicknameCasefolded = client.nickCasefolded
|
result.nick = client.nick
|
||||||
result.nickname = client.nick
|
result.nickCasefolded = client.nickCasefolded
|
||||||
result.username = client.username
|
result.username = client.username
|
||||||
result.hostname = client.hostname
|
result.hostname = client.username
|
||||||
result.realname = client.realname
|
result.realname = client.realname
|
||||||
|
result.nickMask = client.nickMaskString
|
||||||
|
result.nickMaskCasefolded = client.nickMaskCasefolded
|
||||||
|
result.account = client.account
|
||||||
|
result.accountName = client.accountName
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,9 +83,10 @@ func parseCallback(spec string, config *AccountConfig) (callbackNamespace string
|
|||||||
|
|
||||||
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
|
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
|
||||||
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
|
nick := client.Nick()
|
||||||
// clients can't reg new accounts if they're already logged in
|
// clients can't reg new accounts if they're already logged in
|
||||||
if client.LoggedIntoAccount() {
|
if client.LoggedIntoAccount() {
|
||||||
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("You're already logged into an account"))
|
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, "*", client.t("You're already logged into an account"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,12 +95,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
casefoldedAccount, err := CasefoldName(account)
|
casefoldedAccount, err := CasefoldName(account)
|
||||||
// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
|
// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
|
||||||
if err != nil || msg.Params[1] == "*" {
|
if err != nil || msg.Params[1] == "*" {
|
||||||
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, account, client.t("Account name is not valid"))
|
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, account, client.t("Account name is not valid"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msg.Params) < 4 {
|
if len(msg.Params) < 4 {
|
||||||
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
|
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, nick, msg.Command, client.t("Not enough parameters"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +108,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
|
callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
|
||||||
|
|
||||||
if callbackNamespace == "" {
|
if callbackNamespace == "" {
|
||||||
rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackSpec, client.t("Callback namespace is not supported"))
|
rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, nick, account, callbackSpec, client.t("Callback namespace is not supported"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,12 +132,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if credentialType == "certfp" && client.certfp == "" {
|
if credentialType == "certfp" && client.certfp == "" {
|
||||||
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
|
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !credentialValid {
|
if !credentialValid {
|
||||||
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
|
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,6 +147,13 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
} else if credentialType == "passphrase" {
|
} else if credentialType == "passphrase" {
|
||||||
passphrase = credentialValue
|
passphrase = credentialValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throttled, remainingTime := client.loginThrottle.Touch()
|
||||||
|
if throttled {
|
||||||
|
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
|
err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := "Unknown"
|
msg := "Unknown"
|
||||||
@ -161,7 +169,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
|
if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
|
||||||
msg = err.Error()
|
msg = err.Error()
|
||||||
}
|
}
|
||||||
rb.Add(nil, server.name, code, client.nick, "ACC", "REGISTER", client.t(msg))
|
rb.Add(nil, server.name, code, nick, "ACC", "REGISTER", client.t(msg))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +183,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
|||||||
} else {
|
} else {
|
||||||
messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
|
messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
|
||||||
message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
|
message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
|
||||||
rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, client.nick, casefoldedAccount, message)
|
rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, nick, casefoldedAccount, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -336,6 +344,8 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
|
|||||||
|
|
||||||
var accountKey, authzid string
|
var accountKey, authzid string
|
||||||
|
|
||||||
|
nick := client.Nick()
|
||||||
|
|
||||||
if len(splitValue) == 3 {
|
if len(splitValue) == 3 {
|
||||||
accountKey = string(splitValue[0])
|
accountKey = string(splitValue[0])
|
||||||
authzid = string(splitValue[1])
|
authzid = string(splitValue[1])
|
||||||
@ -343,11 +353,17 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
|
|||||||
if accountKey == "" {
|
if accountKey == "" {
|
||||||
accountKey = authzid
|
accountKey = authzid
|
||||||
} else if accountKey != authzid {
|
} else if accountKey != authzid {
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
|
rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: Invalid auth blob"))
|
rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: Invalid auth blob"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
throttled, remainingTime := client.loginThrottle.Touch()
|
||||||
|
if throttled {
|
||||||
|
rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,7 +371,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
|
|||||||
err := server.accounts.AuthenticateByPassphrase(client, accountKey, password)
|
err := server.accounts.AuthenticateByPassphrase(client, accountKey, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := authErrorToMessage(server, err)
|
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)))
|
rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,7 +383,7 @@ func authErrorToMessage(server *Server, err error) (msg string) {
|
|||||||
if err == errAccountDoesNotExist || err == errAccountUnverified || err == errAccountInvalidCredentials {
|
if err == errAccountDoesNotExist || err == errAccountUnverified || err == errAccountInvalidCredentials {
|
||||||
msg = err.Error()
|
msg = err.Error()
|
||||||
} else {
|
} else {
|
||||||
server.logger.Error("internal", fmt.Sprintf("sasl authentication failure: %v", err))
|
server.logger.Error("internal", "sasl authentication failure", err.Error())
|
||||||
msg = "Unknown"
|
msg = "Unknown"
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -1646,7 +1662,7 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
if client.Registered() {
|
if client.Registered() {
|
||||||
performNickChange(server, client, client, msg.Params[0], rb)
|
performNickChange(server, client, client, msg.Params[0], rb)
|
||||||
} else {
|
} else {
|
||||||
client.SetPreregNick(msg.Params[0])
|
client.preregNick = msg.Params[0]
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -2541,7 +2557,7 @@ func whowasHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, whoWas := range results {
|
for _, whoWas := range results {
|
||||||
rb.Add(nil, server.name, RPL_WHOWASUSER, cnick, whoWas.nickname, whoWas.username, whoWas.hostname, "*", whoWas.realname)
|
rb.Add(nil, server.name, RPL_WHOWASUSER, cnick, whoWas.nick, whoWas.username, whoWas.hostname, "*", whoWas.realname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(nickname) > 0 {
|
if len(nickname) > 0 {
|
||||||
|
@ -189,14 +189,14 @@ type NickTimer struct {
|
|||||||
// NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled)
|
// NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled)
|
||||||
func NewNickTimer(client *Client) *NickTimer {
|
func NewNickTimer(client *Client) *NickTimer {
|
||||||
config := client.server.AccountConfig().NickReservation
|
config := client.server.AccountConfig().NickReservation
|
||||||
if !(config.Enabled && config.Method == NickReservationWithTimeout) {
|
if !(config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
nt := NickTimer{
|
|
||||||
|
return &NickTimer{
|
||||||
client: client,
|
client: client,
|
||||||
timeout: config.RenameTimeout,
|
timeout: config.RenameTimeout,
|
||||||
}
|
}
|
||||||
return &nt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Touch records a nick change and updates the timer as necessary
|
// Touch records a nick change and updates the timer as necessary
|
||||||
@ -207,7 +207,8 @@ func (nt *NickTimer) Touch() {
|
|||||||
|
|
||||||
nick := nt.client.NickCasefolded()
|
nick := nt.client.NickCasefolded()
|
||||||
account := nt.client.Account()
|
account := nt.client.Account()
|
||||||
accountForNick := nt.client.server.accounts.NickToAccount(nick)
|
accountForNick, method := nt.client.server.accounts.EnforcementStatus(nick)
|
||||||
|
enforceTimeout := method == NickReservationWithTimeout
|
||||||
|
|
||||||
var shouldWarn bool
|
var shouldWarn bool
|
||||||
|
|
||||||
@ -227,11 +228,11 @@ func (nt *NickTimer) Touch() {
|
|||||||
nt.accountForNick = accountForNick
|
nt.accountForNick = accountForNick
|
||||||
delinquent := accountForNick != "" && accountForNick != account
|
delinquent := accountForNick != "" && accountForNick != account
|
||||||
|
|
||||||
if nt.timer != nil && (!delinquent || accountChanged) {
|
if nt.timer != nil && (!enforceTimeout || !delinquent || accountChanged) {
|
||||||
nt.timer.Stop()
|
nt.timer.Stop()
|
||||||
nt.timer = nil
|
nt.timer = nil
|
||||||
}
|
}
|
||||||
if delinquent && accountChanged {
|
if enforceTimeout && delinquent && accountChanged {
|
||||||
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
|
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
|
||||||
shouldWarn = true
|
shouldWarn = true
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ package logger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@ -32,6 +33,20 @@ const (
|
|||||||
LogError
|
LogError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
colorTimeGrey = ansi.ColorFunc("243")
|
||||||
|
colorGrey = ansi.ColorFunc("8")
|
||||||
|
colorAlert = ansi.ColorFunc("232+b:red")
|
||||||
|
colorWarn = ansi.ColorFunc("black:214")
|
||||||
|
colorInfo = ansi.ColorFunc("117")
|
||||||
|
colorDebug = ansi.ColorFunc("78")
|
||||||
|
colorSection = ansi.ColorFunc("229")
|
||||||
|
separator = colorGrey(":")
|
||||||
|
|
||||||
|
colorableStdout = colorable.NewColorableStdout()
|
||||||
|
colorableStderr = colorable.NewColorableStderr()
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// LogLevelNames takes a config name and gives the real log level.
|
// LogLevelNames takes a config name and gives the real log level.
|
||||||
LogLevelNames = map[string]Level{
|
LogLevelNames = map[string]Level{
|
||||||
@ -230,51 +245,56 @@ func (logger *singleLogger) Log(level Level, logType string, messageParts ...str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// assemble full line
|
// assemble full line
|
||||||
timeGrey := ansi.ColorFunc("243")
|
|
||||||
grey := ansi.ColorFunc("8")
|
|
||||||
alert := ansi.ColorFunc("232+b:red")
|
|
||||||
warn := ansi.ColorFunc("black:214")
|
|
||||||
info := ansi.ColorFunc("117")
|
|
||||||
debug := ansi.ColorFunc("78")
|
|
||||||
section := ansi.ColorFunc("229")
|
|
||||||
|
|
||||||
levelDisplay := LogLevelDisplayNames[level]
|
levelDisplay := LogLevelDisplayNames[level]
|
||||||
if level == LogError {
|
if level == LogError {
|
||||||
levelDisplay = alert(levelDisplay)
|
levelDisplay = colorAlert(levelDisplay)
|
||||||
} else if level == LogWarning {
|
} else if level == LogWarning {
|
||||||
levelDisplay = warn(levelDisplay)
|
levelDisplay = colorWarn(levelDisplay)
|
||||||
} else if level == LogInfo {
|
} else if level == LogInfo {
|
||||||
levelDisplay = info(levelDisplay)
|
levelDisplay = colorInfo(levelDisplay)
|
||||||
} else if level == LogDebug {
|
} else if level == LogDebug {
|
||||||
levelDisplay = debug(levelDisplay)
|
levelDisplay = colorDebug(levelDisplay)
|
||||||
}
|
}
|
||||||
|
|
||||||
sep := grey(":")
|
var formattedBuf, rawBuf bytes.Buffer
|
||||||
fullStringFormatted := fmt.Sprintf("%s %s %s %s %s %s ", timeGrey(time.Now().UTC().Format("2006-01-02T15:04:05.000Z")), sep, levelDisplay, sep, section(logType), sep)
|
fmt.Fprintf(&formattedBuf, "%s %s %s %s %s %s ", colorTimeGrey(time.Now().UTC().Format("2006-01-02T15:04:05.000Z")), separator, levelDisplay, separator, colorSection(logType), separator)
|
||||||
fullStringRaw := fmt.Sprintf("%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType)
|
if logger.MethodFile.Enabled {
|
||||||
for i, p := range messageParts {
|
fmt.Fprintf(&rawBuf, "%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType)
|
||||||
fullStringFormatted += p
|
|
||||||
fullStringRaw += p
|
|
||||||
if i != len(messageParts)-1 {
|
|
||||||
fullStringFormatted += " " + sep + " "
|
|
||||||
fullStringRaw += " : "
|
|
||||||
}
|
}
|
||||||
|
for i, p := range messageParts {
|
||||||
|
formattedBuf.WriteString(p)
|
||||||
|
if logger.MethodFile.Enabled {
|
||||||
|
rawBuf.WriteString(p)
|
||||||
|
}
|
||||||
|
if i != len(messageParts)-1 {
|
||||||
|
formattedBuf.WriteRune(' ')
|
||||||
|
formattedBuf.WriteString(separator)
|
||||||
|
formattedBuf.WriteRune(' ')
|
||||||
|
if logger.MethodFile.Enabled {
|
||||||
|
rawBuf.WriteString(" : ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formattedBuf.WriteRune('\n')
|
||||||
|
if logger.MethodFile.Enabled {
|
||||||
|
rawBuf.WriteRune('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// output
|
// output
|
||||||
if logger.MethodSTDOUT {
|
if logger.MethodSTDOUT {
|
||||||
logger.stdoutWriteLock.Lock()
|
logger.stdoutWriteLock.Lock()
|
||||||
fmt.Fprintln(colorable.NewColorableStdout(), fullStringFormatted)
|
colorableStdout.Write(formattedBuf.Bytes())
|
||||||
logger.stdoutWriteLock.Unlock()
|
logger.stdoutWriteLock.Unlock()
|
||||||
}
|
}
|
||||||
if logger.MethodSTDERR {
|
if logger.MethodSTDERR {
|
||||||
logger.stdoutWriteLock.Lock()
|
logger.stdoutWriteLock.Lock()
|
||||||
fmt.Fprintln(colorable.NewColorableStderr(), fullStringFormatted)
|
colorableStderr.Write(formattedBuf.Bytes())
|
||||||
logger.stdoutWriteLock.Unlock()
|
logger.stdoutWriteLock.Unlock()
|
||||||
}
|
}
|
||||||
if logger.MethodFile.Enabled {
|
if logger.MethodFile.Enabled {
|
||||||
logger.fileWriteLock.Lock()
|
logger.fileWriteLock.Lock()
|
||||||
logger.MethodFile.Writer.WriteString(fullStringRaw + "\n")
|
logger.MethodFile.Writer.Write(rawBuf.Bytes())
|
||||||
logger.MethodFile.Writer.Flush()
|
logger.MethodFile.Writer.Flush()
|
||||||
logger.fileWriteLock.Unlock()
|
logger.fileWriteLock.Unlock()
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
|
|||||||
|
|
||||||
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
|
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
|
||||||
if hadNick {
|
if hadNick {
|
||||||
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nickname, nickname))
|
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname))
|
||||||
target.server.whoWas.Append(whowas)
|
target.server.whoWas.Append(whowas)
|
||||||
for friend := range target.Friends() {
|
for friend := range target.Friends() {
|
||||||
friend.Send(nil, origNickMask, "NICK", nickname)
|
friend.Send(nil, origNickMask, "NICK", nickname)
|
||||||
|
@ -25,6 +25,11 @@ func nsGroupEnabled(server *Server) bool {
|
|||||||
return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled
|
return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nsEnforceEnabled(server *Server) bool {
|
||||||
|
config := server.Config()
|
||||||
|
return config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement
|
||||||
|
}
|
||||||
|
|
||||||
const nickservHelp = `NickServ lets you register and login to an account.
|
const nickservHelp = `NickServ lets you register and login to an account.
|
||||||
|
|
||||||
To see in-depth help for a specific NickServ command, try:
|
To see in-depth help for a specific NickServ command, try:
|
||||||
@ -44,6 +49,22 @@ DROP de-links the given (or your current) nickname from your user account.`,
|
|||||||
enabled: servCmdRequiresAccreg,
|
enabled: servCmdRequiresAccreg,
|
||||||
authRequired: true,
|
authRequired: true,
|
||||||
},
|
},
|
||||||
|
"enforce": {
|
||||||
|
handler: nsEnforceHandler,
|
||||||
|
help: `Syntax: $bENFORCE [method]$b
|
||||||
|
|
||||||
|
ENFORCE lets you specify a custom enforcement mechanism for your registered
|
||||||
|
nicknames. Your options are:
|
||||||
|
1. 'none' [no enforcement, overriding the server default]
|
||||||
|
2. 'timeout' [anyone using the nick must authenticate before a deadline,
|
||||||
|
or else they will be renamed]
|
||||||
|
3. 'strict' [you must already be authenticated to use the nick]
|
||||||
|
4. 'default' [use the server default]
|
||||||
|
With no arguments, queries your current enforcement status.`,
|
||||||
|
helpShort: `$bENFORCE$b lets you change how your nicknames are reserved.`,
|
||||||
|
authRequired: true,
|
||||||
|
enabled: nsEnforceEnabled,
|
||||||
|
},
|
||||||
"ghost": {
|
"ghost": {
|
||||||
handler: nsGhostHandler,
|
handler: nsGhostHandler,
|
||||||
help: `Syntax: $bGHOST <nickname>$b
|
help: `Syntax: $bGHOST <nickname>$b
|
||||||
@ -200,6 +221,15 @@ func nsGroupHandler(server *Server, client *Client, command, params string, rb *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) {
|
||||||
|
throttled, remainingTime := client.loginThrottle.Touch()
|
||||||
|
if throttled {
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||||
loginSuccessful := false
|
loginSuccessful := false
|
||||||
|
|
||||||
@ -207,6 +237,9 @@ func nsIdentifyHandler(server *Server, client *Client, command, params string, r
|
|||||||
|
|
||||||
// try passphrase
|
// try passphrase
|
||||||
if username != "" && passphrase != "" {
|
if username != "" && passphrase != "" {
|
||||||
|
if !nsLoginThrottleCheck(client, rb) {
|
||||||
|
return
|
||||||
|
}
|
||||||
err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
|
err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
|
||||||
loginSuccessful = (err == nil)
|
loginSuccessful = (err == nil)
|
||||||
}
|
}
|
||||||
@ -407,10 +440,15 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb
|
|||||||
var newPassword string
|
var newPassword string
|
||||||
var errorMessage string
|
var errorMessage string
|
||||||
|
|
||||||
|
hasPrivs := client.HasRoleCapabs("accreg")
|
||||||
|
if !hasPrivs && !nsLoginThrottleCheck(client, rb) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fields := strings.Fields(params)
|
fields := strings.Fields(params)
|
||||||
switch len(fields) {
|
switch len(fields) {
|
||||||
case 2:
|
case 2:
|
||||||
if !client.HasRoleCapabs("accreg") {
|
if !hasPrivs {
|
||||||
errorMessage = "Insufficient privileges"
|
errorMessage = "Insufficient privileges"
|
||||||
} else {
|
} else {
|
||||||
target, newPassword = fields[0], fields[1]
|
target, newPassword = fields[0], fields[1]
|
||||||
@ -443,7 +481,29 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
nsNotice(rb, client.t("Password changed"))
|
nsNotice(rb, client.t("Password changed"))
|
||||||
} else {
|
} else {
|
||||||
server.logger.Error("internal", fmt.Sprintf("could not upgrade user password: %v", err))
|
server.logger.Error("internal", "could not upgrade user password:", err.Error())
|
||||||
nsNotice(rb, client.t("Password could not be changed due to server error"))
|
nsNotice(rb, client.t("Password could not be changed due to server error"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nsEnforceHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||||
|
arg := strings.TrimSpace(params)
|
||||||
|
|
||||||
|
if arg == "" {
|
||||||
|
status := server.accounts.getStoredEnforcementStatus(client.Account())
|
||||||
|
nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status))
|
||||||
|
} else {
|
||||||
|
method, err := nickReservationFromString(arg)
|
||||||
|
if err != nil {
|
||||||
|
nsNotice(rb, client.t("Invalid parameters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = server.accounts.SetEnforcementStatus(client.Account(), method)
|
||||||
|
if err == nil {
|
||||||
|
nsNotice(rb, client.t("Enforcement method set"))
|
||||||
|
} else {
|
||||||
|
server.logger.Error("internal", "couldn't store NS ENFORCE data", err.Error())
|
||||||
|
nsNotice(rb, client.t("An error occurred"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -386,8 +386,7 @@ func (server *Server) tryRegister(c *Client) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
preregNick := c.PreregNick()
|
if c.preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState {
|
||||||
if preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,10 +399,10 @@ func (server *Server) tryRegister(c *Client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rb := NewResponseBuffer(c)
|
rb := NewResponseBuffer(c)
|
||||||
nickAssigned := performNickChange(server, c, c, preregNick, rb)
|
nickAssigned := performNickChange(server, c, c, c.preregNick, rb)
|
||||||
rb.Send(true)
|
rb.Send(true)
|
||||||
if !nickAssigned {
|
if !nickAssigned {
|
||||||
c.SetPreregNick("")
|
c.preregNick = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,9 +499,9 @@ func (client *Client) WhoisChannelsNames(target *Client) []string {
|
|||||||
|
|
||||||
func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
||||||
cnick := client.Nick()
|
cnick := client.Nick()
|
||||||
targetInfo := target.WhoWas()
|
targetInfo := target.Details()
|
||||||
rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nickname, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname)
|
rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nick, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname)
|
||||||
tnick := targetInfo.nickname
|
tnick := targetInfo.nick
|
||||||
|
|
||||||
whoischannels := client.WhoisChannelsNames(target)
|
whoischannels := client.WhoisChannelsNames(target)
|
||||||
if whoischannels != nil {
|
if whoischannels != nil {
|
||||||
@ -518,9 +517,8 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
|||||||
if target.HasMode(modes.TLS) {
|
if target.HasMode(modes.TLS) {
|
||||||
rb.Add(nil, client.server.name, RPL_WHOISSECURE, cnick, tnick, client.t("is using a secure connection"))
|
rb.Add(nil, client.server.name, RPL_WHOISSECURE, cnick, tnick, client.t("is using a secure connection"))
|
||||||
}
|
}
|
||||||
taccount := target.AccountName()
|
if targetInfo.accountName != "*" {
|
||||||
if taccount != "*" {
|
rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, cnick, tnick, targetInfo.accountName, client.t("is logged in as"))
|
||||||
rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, cnick, tnick, taccount, client.t("is logged in as"))
|
|
||||||
}
|
}
|
||||||
if target.HasMode(modes.Bot) {
|
if target.HasMode(modes.Bot) {
|
||||||
rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.Config().Network.Name)))
|
rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.Config().Network.Name)))
|
||||||
@ -836,7 +834,7 @@ func (server *Server) setupPprofListener(config *Config) {
|
|||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if err := ps.ListenAndServe(); err != nil {
|
if err := ps.ListenAndServe(); err != nil {
|
||||||
server.logger.Error("rehash", fmt.Sprintf("pprof listener failed: %v", err))
|
server.logger.Error("rehash", "pprof listener failed", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
server.pprofServer = &ps
|
server.pprofServer = &ps
|
||||||
|
@ -22,15 +22,6 @@ type WhoWasList struct {
|
|||||||
accessMutex sync.RWMutex // tier 1
|
accessMutex sync.RWMutex // tier 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// WhoWas is an entry in the WhoWasList.
|
|
||||||
type WhoWas struct {
|
|
||||||
nicknameCasefolded string
|
|
||||||
nickname string
|
|
||||||
username string
|
|
||||||
hostname string
|
|
||||||
realname string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWhoWasList returns a new WhoWasList
|
// NewWhoWasList returns a new WhoWasList
|
||||||
func NewWhoWasList(size int) *WhoWasList {
|
func NewWhoWasList(size int) *WhoWasList {
|
||||||
return &WhoWasList{
|
return &WhoWasList{
|
||||||
@ -82,7 +73,7 @@ func (list *WhoWasList) Find(nickname string, limit int) (results []WhoWas) {
|
|||||||
// iterate backwards through the ring buffer
|
// iterate backwards through the ring buffer
|
||||||
pos := list.prev(list.end)
|
pos := list.prev(list.end)
|
||||||
for limit == 0 || len(results) < limit {
|
for limit == 0 || len(results) < limit {
|
||||||
if casefoldedNickname == list.buffer[pos].nicknameCasefolded {
|
if casefoldedNickname == list.buffer[pos].nickCasefolded {
|
||||||
results = append(results, list.buffer[pos])
|
results = append(results, list.buffer[pos])
|
||||||
}
|
}
|
||||||
if pos == list.start {
|
if pos == list.start {
|
||||||
|
@ -13,8 +13,8 @@ func makeTestWhowas(nick string) WhoWas {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return WhoWas{
|
return WhoWas{
|
||||||
nicknameCasefolded: cfnick,
|
nickCasefolded: cfnick,
|
||||||
nickname: nick,
|
nick: nick,
|
||||||
username: "user",
|
username: "user",
|
||||||
hostname: "oragono.io",
|
hostname: "oragono.io",
|
||||||
realname: "Real Name",
|
realname: "Real Name",
|
||||||
@ -36,48 +36,48 @@ func TestWhoWas(t *testing.T) {
|
|||||||
t.Fatalf("incorrect whowas results: %v", results)
|
t.Fatalf("incorrect whowas results: %v", results)
|
||||||
}
|
}
|
||||||
results = wwl.Find("dan-", 10)
|
results = wwl.Find("dan-", 10)
|
||||||
if len(results) != 1 || results[0].nickname != "dan-" {
|
if len(results) != 1 || results[0].nick != "dan-" {
|
||||||
t.Fatalf("incorrect whowas results: %v", results)
|
t.Fatalf("incorrect whowas results: %v", results)
|
||||||
}
|
}
|
||||||
|
|
||||||
wwl.Append(makeTestWhowas("slingamn"))
|
wwl.Append(makeTestWhowas("slingamn"))
|
||||||
results = wwl.Find("slingamN", 10)
|
results = wwl.Find("slingamN", 10)
|
||||||
if len(results) != 1 || results[0].nickname != "slingamn" {
|
if len(results) != 1 || results[0].nick != "slingamn" {
|
||||||
t.Fatalf("incorrect whowas results: %v", results)
|
t.Fatalf("incorrect whowas results: %v", results)
|
||||||
}
|
}
|
||||||
|
|
||||||
wwl.Append(makeTestWhowas("Dan-"))
|
wwl.Append(makeTestWhowas("Dan-"))
|
||||||
results = wwl.Find("dan-", 10)
|
results = wwl.Find("dan-", 10)
|
||||||
// reverse chronological order
|
// reverse chronological order
|
||||||
if len(results) != 2 || results[0].nickname != "Dan-" || results[1].nickname != "dan-" {
|
if len(results) != 2 || results[0].nick != "Dan-" || results[1].nick != "dan-" {
|
||||||
t.Fatalf("incorrect whowas results: %v", results)
|
t.Fatalf("incorrect whowas results: %v", results)
|
||||||
}
|
}
|
||||||
// 0 means no limit
|
// 0 means no limit
|
||||||
results = wwl.Find("dan-", 0)
|
results = wwl.Find("dan-", 0)
|
||||||
if len(results) != 2 || results[0].nickname != "Dan-" || results[1].nickname != "dan-" {
|
if len(results) != 2 || results[0].nick != "Dan-" || results[1].nick != "dan-" {
|
||||||
t.Fatalf("incorrect whowas results: %v", results)
|
t.Fatalf("incorrect whowas results: %v", results)
|
||||||
}
|
}
|
||||||
// a limit of 1 should return the most recent entry only
|
// a limit of 1 should return the most recent entry only
|
||||||
results = wwl.Find("dan-", 1)
|
results = wwl.Find("dan-", 1)
|
||||||
if len(results) != 1 || results[0].nickname != "Dan-" {
|
if len(results) != 1 || results[0].nick != "Dan-" {
|
||||||
t.Fatalf("incorrect whowas results: %v", results)
|
t.Fatalf("incorrect whowas results: %v", results)
|
||||||
}
|
}
|
||||||
|
|
||||||
wwl.Append(makeTestWhowas("moocow"))
|
wwl.Append(makeTestWhowas("moocow"))
|
||||||
results = wwl.Find("moocow", 10)
|
results = wwl.Find("moocow", 10)
|
||||||
if len(results) != 1 || results[0].nickname != "moocow" {
|
if len(results) != 1 || results[0].nick != "moocow" {
|
||||||
t.Fatalf("incorrect whowas results: %v", results)
|
t.Fatalf("incorrect whowas results: %v", results)
|
||||||
}
|
}
|
||||||
results = wwl.Find("dan-", 10)
|
results = wwl.Find("dan-", 10)
|
||||||
// should have overwritten the original entry, leaving the second
|
// should have overwritten the original entry, leaving the second
|
||||||
if len(results) != 1 || results[0].nickname != "Dan-" {
|
if len(results) != 1 || results[0].nick != "Dan-" {
|
||||||
t.Fatalf("incorrect whowas results: %v", results)
|
t.Fatalf("incorrect whowas results: %v", results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// overwrite the second entry
|
// overwrite the second entry
|
||||||
wwl.Append(makeTestWhowas("enckse"))
|
wwl.Append(makeTestWhowas("enckse"))
|
||||||
results = wwl.Find("enckse", 10)
|
results = wwl.Find("enckse", 10)
|
||||||
if len(results) != 1 || results[0].nickname != "enckse" {
|
if len(results) != 1 || results[0].nick != "enckse" {
|
||||||
t.Fatalf("incorrect whowas results: %v", results)
|
t.Fatalf("incorrect whowas results: %v", results)
|
||||||
}
|
}
|
||||||
results = wwl.Find("slingamn", 10)
|
results = wwl.Find("slingamn", 10)
|
||||||
|
18
oragono.yaml
18
oragono.yaml
@ -179,6 +179,17 @@ accounts:
|
|||||||
# is account authentication enabled?
|
# is account authentication enabled?
|
||||||
authentication-enabled: true
|
authentication-enabled: true
|
||||||
|
|
||||||
|
# throttle account login attempts (to prevent either password guessing, or DoS
|
||||||
|
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
|
||||||
|
login-throttling:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# window
|
||||||
|
duration: 1m
|
||||||
|
|
||||||
|
# number of attempts allowed within the window
|
||||||
|
max-attempts: 3
|
||||||
|
|
||||||
# some clients (notably Pidgin and Hexchat) offer only a single password field,
|
# some clients (notably Pidgin and Hexchat) offer only a single password field,
|
||||||
# which makes it impossible to specify a separate server password (for the PASS
|
# which makes it impossible to specify a separate server password (for the PASS
|
||||||
# command) and SASL password. if this option is set to true, a client that
|
# command) and SASL password. if this option is set to true, a client that
|
||||||
@ -195,12 +206,19 @@ accounts:
|
|||||||
additional-nick-limit: 2
|
additional-nick-limit: 2
|
||||||
|
|
||||||
# method describes how nickname reservation is handled
|
# method describes how nickname reservation is handled
|
||||||
|
# already logged-in using SASL or NickServ
|
||||||
# timeout: let the user change to the registered nickname, give them X seconds
|
# 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
|
# to login and then rename them if they haven't done so
|
||||||
# strict: don't let the user change to the registered nickname unless they're
|
# strict: don't let the user change to the registered nickname unless they're
|
||||||
# already logged-in using SASL or NickServ
|
# already logged-in using SASL or NickServ
|
||||||
|
# optional: no enforcement by default, but allow users to opt in to
|
||||||
|
# the enforcement level of their choice
|
||||||
method: timeout
|
method: timeout
|
||||||
|
|
||||||
|
# allow users to set their own nickname enforcement status, e.g.,
|
||||||
|
# to opt in to strict enforcement
|
||||||
|
allow-custom-enforcement: true
|
||||||
|
|
||||||
# rename-timeout - this is how long users have 'til they're renamed
|
# rename-timeout - this is how long users have 'til they're renamed
|
||||||
rename-timeout: 30s
|
rename-timeout: 30s
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user