From 895a0e6d688f3cbee96916834334de15190d362b Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Fri, 27 Mar 2020 17:52:37 -0400 Subject: [PATCH] fix #913 --- irc/accounts.go | 33 +++++++++++++++++++++++++++++++++ irc/config.go | 38 +++++++++++++++++++++++++++----------- irc/errors.go | 1 + irc/handlers.go | 6 +++--- irc/nickserv.go | 27 ++++++++++++--------------- irc/server.go | 3 +++ oragono.yaml | 8 ++++++++ 7 files changed, 87 insertions(+), 29 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index d67683f2..0626e4b7 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -15,6 +15,7 @@ import ( "unicode" "github.com/oragono/oragono/irc/caps" + "github.com/oragono/oragono/irc/connection_limits" "github.com/oragono/oragono/irc/ldap" "github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/utils" @@ -62,6 +63,7 @@ type AccountManager struct { nickToAccount map[string]string skeletonToAccount map[string]string accountToMethod map[string]NickEnforcementMethod + registerThrottle connection_limits.GenericThrottle } func (am *AccountManager) Initialize(server *Server) { @@ -75,6 +77,24 @@ func (am *AccountManager) Initialize(server *Server) { am.buildNickToAccountIndex(config) am.initVHostRequestQueue(config) am.createAlwaysOnClients(config) + am.resetRegisterThrottle(config) +} + +func (am *AccountManager) resetRegisterThrottle(config *Config) { + am.Lock() + defer am.Unlock() + + am.registerThrottle = connection_limits.GenericThrottle{ + Duration: config.Accounts.Registration.Throttling.Duration, + Limit: config.Accounts.Registration.Throttling.MaxAttempts, + } +} + +func (am *AccountManager) touchRegisterThrottle() (throttled bool) { + am.Lock() + defer am.Unlock() + throttled, _ = am.registerThrottle.Touch() + return } func (am *AccountManager) createAlwaysOnClients(config *Config) { @@ -363,6 +383,15 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames return errFeatureDisabled } + if client != nil && client.Account() != "" { + return errAccountAlreadyLoggedIn + } + + if client != nil && am.touchRegisterThrottle() { + am.server.logger.Warning("accounts", "global registration throttle exceeded by client", client.Nick()) + return errLimitExceeded + } + // if nick reservation is enabled, you can only register your current nickname // as an account; this prevents "land-grab" situations where someone else // registers your nick out from under you and then NS GHOSTs you @@ -725,6 +754,10 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er return errAccountVerificationFailed } + if client != nil && client.Account() != "" { + return errAccountAlreadyLoggedIn + } + verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount) accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount) diff --git a/irc/config.go b/irc/config.go index d7407b81..34ab354b 100644 --- a/irc/config.go +++ b/irc/config.go @@ -229,6 +229,29 @@ type MulticlientConfig struct { AlwaysOn PersistentStatus `yaml:"always-on"` } +type throttleConfig struct { + Enabled bool + Duration time.Duration + MaxAttempts int `yaml:"max-attempts"` +} + +type ThrottleConfig struct { + throttleConfig +} + +func (t *ThrottleConfig) UnmarshalYAML(unmarshal func(interface{}) error) (err error) { + // note that this technique only works if the zero value of the struct + // doesn't need any postprocessing (because if the field is omitted entirely + // from the YAML, then UnmarshalYAML won't be called at all) + if err = unmarshal(&t.throttleConfig); err != nil { + return + } + if !t.Enabled { + t.MaxAttempts = 0 // limit of 0 means disabled + } + return +} + type AccountConfig struct { Registration AccountRegistrationConfig AuthenticationEnabled bool `yaml:"authentication-enabled"` @@ -237,13 +260,9 @@ type AccountConfig struct { Exempted []string exemptedNets []net.IPNet } `yaml:"require-sasl"` - LDAP ldap.ServerConfig - LoginThrottling struct { - Enabled bool - Duration time.Duration - MaxAttempts int `yaml:"max-attempts"` - } `yaml:"login-throttling"` - SkipServerPassword bool `yaml:"skip-server-password"` + LDAP ldap.ServerConfig + LoginThrottling ThrottleConfig `yaml:"login-throttling"` + SkipServerPassword bool `yaml:"skip-server-password"` NickReservation struct { Enabled bool AdditionalNickLimit int `yaml:"additional-nick-limit"` @@ -266,6 +285,7 @@ type AccountConfig struct { // AccountRegistrationConfig controls account registration. type AccountRegistrationConfig struct { Enabled bool + Throttling ThrottleConfig EnabledCallbacks []string `yaml:"enabled-callbacks"` EnabledCredentialTypes []string `yaml:"-"` VerifyTimeout custime.Duration `yaml:"verify-timeout"` @@ -997,10 +1017,6 @@ func LoadConfig(filename string) (config *Config, err error) { } } - if !config.Accounts.LoginThrottling.Enabled { - config.Accounts.LoginThrottling.MaxAttempts = 0 // limit of 0 means disabled - } - config.Server.capValues[caps.SASL] = "PLAIN,EXTERNAL" if !config.Accounts.AuthenticationEnabled { config.Server.supportedCaps.Disable(caps.SASL) diff --git a/irc/errors.go b/irc/errors.go index 030884ba..f522a3aa 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -22,6 +22,7 @@ var ( errAccountBadPassphrase = errors.New(`Passphrase contains forbidden characters or is otherwise invalid`) errAccountNickReservationFailed = errors.New("Could not (un)reserve nick") errAccountNotLoggedIn = errors.New("You're not logged into an account") + errAccountAlreadyLoggedIn = errors.New("You're already logged into an account") errAccountTooManyNicks = errors.New("Account has too many reserved nicks") errAccountUnverified = errors.New(`Account is not yet verified`) errAccountVerificationFailed = errors.New("Account verification failed") diff --git a/irc/handlers.go b/irc/handlers.go index 11c57c5b..022ad374 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -66,10 +66,10 @@ func registrationErrorToMessageAndCode(err error) (message, code string) { case errAccountBadPassphrase: code = "REG_INVALID_CREDENTIAL" message = err.Error() - case errAccountAlreadyRegistered, errAccountAlreadyVerified, errAccountAlreadyUnregistered: - message = err.Error() - case errAccountCreation, errAccountMustHoldNick, errAccountBadPassphrase, errCertfpAlreadyExists, errFeatureDisabled: + case errAccountAlreadyRegistered, errAccountAlreadyVerified, errAccountAlreadyUnregistered, errAccountAlreadyLoggedIn, errAccountCreation, errAccountMustHoldNick, errAccountBadPassphrase, errCertfpAlreadyExists, errFeatureDisabled: message = err.Error() + case errLimitExceeded: + message = `There have been too many registration attempts recently; try again later` } return } diff --git a/irc/nickserv.go b/irc/nickserv.go index ac699512..394b4883 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -616,7 +616,9 @@ func nsGroupHandler(server *Server, client *Client, command string, params []str } func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) { + client.stateMutex.Lock() throttled, remainingTime := client.loginThrottle.Touch() + client.stateMutex.Unlock() if throttled { nsNotice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime)) return false @@ -741,11 +743,6 @@ func nsRegisterHandler(server *Server, client *Client, command string, params [] } } - if details.account != "" { - nsNotice(rb, client.t("You're already logged into an account")) - return - } - if !nsLoginThrottleCheck(client, rb) { return } @@ -793,13 +790,10 @@ func nsRegisterHandler(server *Server, client *Client, command string, params [] message := fmt.Sprintf(messageTemplate, fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)) nsNotice(rb, message) } - } - - // details could not be stored and relevant numerics have been dispatched, abort - message, _ := registrationErrorToMessageAndCode(err) - if err != nil { + } else { + // details could not be stored and relevant numerics have been dispatched, abort + message, _ := registrationErrorToMessageAndCode(err) nsNotice(rb, client.t(message)) - return } } @@ -894,10 +888,13 @@ func nsVerifyHandler(server *Server, client *Client, command string, params []st err := server.accounts.Verify(client, username, code) var errorMessage string - if err == errAccountVerificationInvalidCode || err == errAccountAlreadyVerified { - errorMessage = err.Error() - } else if err != nil { - errorMessage = errAccountVerificationFailed.Error() + if err != nil { + switch err { + case errAccountAlreadyLoggedIn, errAccountVerificationInvalidCode, errAccountAlreadyVerified: + errorMessage = err.Error() + default: + errorMessage = errAccountVerificationFailed.Error() + } } if errorMessage != "" { diff --git a/irc/server.go b/irc/server.go index 60e52b56..28e8bd08 100644 --- a/irc/server.go +++ b/irc/server.go @@ -634,6 +634,9 @@ func (server *Server) applyConfig(config *Config) (err error) { client.resizeHistory(config) } } + if oldConfig.Accounts.Registration.Throttling != config.Accounts.Registration.Throttling { + server.accounts.resetRegisterThrottle(config) + } } // activate the new config diff --git a/oragono.yaml b/oragono.yaml index 695a10dd..e5762443 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -266,6 +266,14 @@ accounts: # the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER` enabled: true + # global throttle on new account creation + throttling: + enabled: true + # window + duration: 10m + # number of attempts allowed within the window + max-attempts: 30 + # this is the bcrypt cost we'll use for account passwords bcrypt-cost: 9