This commit is contained in:
Shivaram Lingamneni 2020-10-06 18:04:29 -04:00
parent 509d3f1fdd
commit 9ed789f67c
15 changed files with 285 additions and 108 deletions

View File

@ -313,6 +313,9 @@ accounts:
# the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER`
enabled: true
# can users use the REGISTER command to register before fully connecting?
allow-before-connect: true
# global throttle on new account creation
throttling:
enabled: true
@ -327,28 +330,26 @@ accounts:
# length of time a user has to verify their account before it can be re-registered
verify-timeout: "32h"
# callbacks to allow
enabled-callbacks:
- none # no verification needed, will instantly register successfully
# example configuration for sending verification emails
# callbacks:
# mailto:
# sender: "admin@my.network"
# require-tls: true
# helo-domain: "my.network" # defaults to server name if unset
# dkim:
# domain: "my.network"
# selector: "20200229"
# key-file: "dkim.pem"
# # to use an MTA/smarthost instead of sending email directly:
# # mta:
# # server: localhost
# # port: 25
# # username: "admin"
# # password: "hunter2"
# blacklist-regexes:
# # - ".*@mailinator.com"
# options for email verification of account registrations
email-verification:
enabled: false
sender: "admin@my.network"
require-tls: true
helo-domain: "my.network" # defaults to server name if unset
# options to enable DKIM signing of outgoing emails (recommended, but
# requires creating a DNS entry for the public key):
# dkim:
# domain: "my.network"
# selector: "20200229"
# key-file: "dkim.pem"
# to use an MTA/smarthost instead of sending email directly:
# mta:
# server: localhost
# port: 25
# username: "admin"
# password: "hunter2"
blacklist-regexes:
# - ".*@mailinator.com"
# throttle account login attempts (to prevent either password guessing, or DoS
# attacks on the server aimed at forcing repeated expensive bcrypt computations)

View File

@ -341,6 +341,9 @@ accounts:
# the `accreg` capability can still create accounts with `/NICKSERV SAREGISTER`
enabled: true
# can users use the REGISTER command to register before fully connecting?
allow-before-connect: true
# global throttle on new account creation
throttling:
enabled: true
@ -355,28 +358,26 @@ accounts:
# length of time a user has to verify their account before it can be re-registered
verify-timeout: "32h"
# callbacks to allow
enabled-callbacks:
- none # no verification needed, will instantly register successfully
# example configuration for sending verification emails
# callbacks:
# mailto:
# sender: "admin@my.network"
# require-tls: true
# helo-domain: "my.network" # defaults to server name if unset
# dkim:
# domain: "my.network"
# selector: "20200229"
# key-file: "dkim.pem"
# # to use an MTA/smarthost instead of sending email directly:
# # mta:
# # server: localhost
# # port: 25
# # username: "admin"
# # password: "hunter2"
# blacklist-regexes:
# # - ".*@mailinator.com"
# options for email verification of account registrations
email-verification:
enabled: false
sender: "admin@my.network"
require-tls: true
helo-domain: "my.network" # defaults to server name if unset
# options to enable DKIM signing of outgoing emails (recommended, but
# requires creating a DNS entry for the public key):
# dkim:
# domain: "my.network"
# selector: "20200229"
# key-file: "dkim.pem"
# to use an MTA/smarthost instead of sending email directly:
# mta:
# server: localhost
# port: 25
# username: "admin"
# password: "hunter2"
blacklist-regexes:
# - ".*@mailinator.com"
# throttle account login attempts (to prevent either password guessing, or DoS
# attacks on the server aimed at forcing repeated expensive bcrypt computations)

View File

@ -177,6 +177,12 @@ CAPDEFS = [
url="https://github.com/ircv3/ircv3-specifications/pull/393",
standard="proposed IRCv3",
),
CapDef(
identifier="Register",
name="draft/register",
url="https://gist.github.com/edk0/bf3b50fc219fd1bed1aa15d98bfb6495",
standard="proposed IRCv3",
),
]
def validate_defs():

View File

@ -410,10 +410,22 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
// n.b. if ForceGuestFormat, then there's no concern, because you can't
// register a guest nickname anyway, and the actual registration system
// will prevent any double-register
if client != nil && config.Accounts.NickReservation.Enabled &&
!config.Accounts.NickReservation.ForceGuestFormat &&
client.NickCasefolded() != casefoldedAccount {
return errAccountMustHoldNick
if client != nil {
if client.registered {
if config.Accounts.NickReservation.Enabled &&
!config.Accounts.NickReservation.ForceGuestFormat &&
client.NickCasefolded() != casefoldedAccount {
return errAccountMustHoldNick
}
} else {
// XXX this is a REGISTER command from a client who hasn't completed the
// initial handshake ("connection registration"). Do SetNick with dryRun=true,
// testing whether they are able to claim the nick
_, nickAcquireError, _ := am.server.clients.SetNick(client, nil, account, true)
if !(nickAcquireError == nil || nickAcquireError == errNoop) {
return errAccountMustHoldNick
}
}
}
// can't register a guest nickname
@ -763,7 +775,7 @@ func (am *AccountManager) dispatchCallback(client *Client, account string, callb
}
func (am *AccountManager) dispatchMailtoCallback(client *Client, account string, callbackValue string) (code string, err error) {
config := am.server.Config().Accounts.Registration.Callbacks.Mailto
config := am.server.Config().Accounts.Registration.EmailVerification
code = utils.GenerateSecretToken()
subject := config.VerifyMessageSubject

View File

@ -7,7 +7,7 @@ package caps
const (
// number of recognized capabilities:
numCapabs = 27
numCapabs = 28
// length of the uint64 array that represents the bitset:
bitsetLen = 1
)
@ -57,6 +57,10 @@ const (
// https://github.com/ircv3/ircv3-specifications/pull/398
Multiline Capability = iota
// Register is the proposed IRCv3 capability named "draft/register":
// https://gist.github.com/edk0/bf3b50fc219fd1bed1aa15d98bfb6495
Register Capability = iota
// Relaymsg is the proposed IRCv3 capability named "draft/relaymsg":
// https://github.com/ircv3/ircv3-specifications/pull/417
Relaymsg Capability = iota
@ -136,6 +140,7 @@ var (
"draft/event-playback",
"draft/languages",
"draft/multiline",
"draft/register",
"draft/relaymsg",
"draft/resume-0.5",
"echo-message",

View File

@ -106,6 +106,7 @@ type Client struct {
requireSASLMessage string
requireSASL bool
registered bool
registerCmdSent bool // already sent the draft/register command, can't send it again
registrationTimer *time.Timer
resumeID string
server *Server
@ -441,7 +442,7 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string,
client.resizeHistory(config)
_, err, _ := server.clients.SetNick(client, nil, account.Name)
_, err, _ := server.clients.SetNick(client, nil, account.Name, false)
if err != nil {
server.logger.Error("internal", "could not establish always-on client", account.Name, err.Error())
return

View File

@ -104,7 +104,9 @@ func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err e
}
// SetNick sets a client's nickname, validating it against nicknames in use
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) (setNick string, err error, returnedFromAway bool) {
// XXX: dryRun validates a client's ability to claim a nick, without
// actually claiming it
func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, returnedFromAway bool) {
config := client.server.Config()
var newCfNick, newSkeleton string
@ -236,6 +238,10 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
return "", errNicknameInUse, false
}
if dryRun {
return "", nil, false
}
formercfnick, formerskeleton := client.uniqueIdentifiers()
if changeSuccess := client.SetNick(newNick, newCfNick, newSkeleton); !changeSuccess {
return "", errClientDestroyed, false

View File

@ -256,6 +256,11 @@ func init() {
handler: relaymsgHandler,
minParams: 3,
},
"REGISTER": {
handler: registerHandler,
minParams: 2,
usablePreReg: true,
},
"RENAME": {
handler: renameHandler,
minParams: 2,
@ -336,6 +341,11 @@ func init() {
"USERS": {
handler: usersHandler,
},
"VERIFY": {
handler: verifyHandler,
usablePreReg: true,
minParams: 2,
},
"VERSION": {
handler: versionHandler,
minParams: 0,

View File

@ -16,7 +16,6 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
@ -298,15 +297,18 @@ type AuthScriptConfig 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"`
Callbacks struct {
Enabled bool
AllowBeforeConnect bool `yaml:"allow-before-connect"`
Throttling ThrottleConfig
// new-style (v2.4 email verification config):
EmailVerification email.MailtoConfig `yaml:"email-verification"`
// old-style email verification config, with "callbacks":
LegacyEnabledCallbacks []string `yaml:"enabled-callbacks"`
LegacyCallbacks struct {
Mailto email.MailtoConfig
}
BcryptCost uint `yaml:"bcrypt-cost"`
} `yaml:"callbacks"`
VerifyTimeout custime.Duration `yaml:"verify-timeout"`
BcryptCost uint `yaml:"bcrypt-cost"`
}
type VHostConfig struct {
@ -1049,24 +1051,29 @@ func LoadConfig(filename string) (config *Config, err error) {
}
config.Logging = newLogConfigs
// hardcode this for now
config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"}
mailtoEnabled := false
for i, name := range config.Accounts.Registration.EnabledCallbacks {
if name == "none" {
// we store "none" as "*" internally
config.Accounts.Registration.EnabledCallbacks[i] = "*"
} else if name == "mailto" {
mailtoEnabled = true
}
}
sort.Strings(config.Accounts.Registration.EnabledCallbacks)
if mailtoEnabled {
err := config.Accounts.Registration.Callbacks.Mailto.Postprocess(config.Server.Name)
if config.Accounts.Registration.EmailVerification.Enabled {
err := config.Accounts.Registration.EmailVerification.Postprocess(config.Server.Name)
if err != nil {
return nil, err
}
} else {
// TODO: this processes the legacy "callback" config, clean this up in 2.5 or later
// TODO: also clean up the legacy "inline" MTA config format (from ee05a4324dfde)
mailtoEnabled := false
for _, name := range config.Accounts.Registration.LegacyEnabledCallbacks {
if name == "mailto" {
mailtoEnabled = true
break
}
}
if mailtoEnabled {
config.Accounts.Registration.EmailVerification = config.Accounts.Registration.LegacyCallbacks.Mailto
config.Accounts.Registration.EmailVerification.Enabled = true
err := config.Accounts.Registration.EmailVerification.Postprocess(config.Server.Name)
if err != nil {
return nil, err
}
}
}
config.Accounts.defaultUserModes = ParseDefaultUserModes(config.Accounts.DefaultUserModes)
@ -1104,6 +1111,24 @@ func LoadConfig(filename string) (config *Config, err error) {
config.Server.supportedCaps.Disable(caps.SASL)
}
if !config.Accounts.Registration.Enabled {
config.Server.supportedCaps.Disable(caps.Register)
} else {
var registerValues []string
if config.Accounts.Registration.AllowBeforeConnect {
registerValues = append(registerValues, "before-connect")
}
if config.Accounts.Registration.EmailVerification.Enabled {
registerValues = append(registerValues, "email-required")
}
if config.Accounts.RequireSasl.Enabled {
registerValues = append(registerValues, "account-required")
}
if len(registerValues) != 0 {
config.Server.capValues[caps.Register] = strings.Join(registerValues, ",")
}
}
maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
if err != nil {
return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())

View File

@ -31,6 +31,7 @@ type MailtoConfig struct {
// so server, port, etc. appear directly at top level
// XXX: see https://github.com/go-yaml/yaml/issues/63
MTAConfig `yaml:",inline"`
Enabled bool
Sender string
HeloDomain string `yaml:"helo-domain"`
RequireTLS bool `yaml:"require-tls"`

View File

@ -71,6 +71,7 @@ var (
errWrongChannelKey = errors.New("Cannot join password-protected channel without the password")
errInviteOnly = errors.New("Cannot join invite-only channel without an invite")
errRegisteredOnly = errors.New("Cannot join registered-only channel without an account")
errValidEmailRequired = errors.New("A valid email address is required for account registration")
)
// String Errors

View File

@ -33,28 +33,30 @@ import (
)
// helper function to parse ACC callbacks, e.g., mailto:person@example.com, tel:16505551234
func parseCallback(spec string, config AccountConfig) (callbackNamespace string, callbackValue string) {
callback := strings.ToLower(spec)
if callback == "*" {
func parseCallback(spec string, config *Config) (callbackNamespace string, callbackValue string, err error) {
// XXX if we don't require verification, ignore any callback that was passed here
// (to avoid confusion in the case where the ircd has no mail server configured)
if !config.Accounts.Registration.EmailVerification.Enabled {
callbackNamespace = "*"
} else if strings.Contains(callback, ":") {
callbackValues := strings.SplitN(callback, ":", 2)
callbackNamespace, callbackValue = callbackValues[0], callbackValues[1]
return
}
callback := strings.ToLower(spec)
if colonIndex := strings.IndexByte(callback, ':'); colonIndex != -1 {
callbackNamespace, callbackValue = callback[:colonIndex], callback[colonIndex+1:]
} else {
// "If a callback namespace is not ... provided, the IRC server MUST use mailto""
callbackNamespace = "mailto"
callbackValue = callback
}
// ensure the callback namespace is valid
// need to search callback list, maybe look at using a map later?
for _, name := range config.Registration.EnabledCallbacks {
if callbackNamespace == name {
return
if config.Accounts.Registration.EmailVerification.Enabled {
if callbackNamespace != "mailto" {
err = errValidEmailRequired
} else if strings.IndexByte(callbackValue, '@') < 1 {
err = errValidEmailRequired
}
}
// error value
callbackNamespace = ""
return
}
@ -2398,6 +2400,116 @@ func quitHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
return true
}
// REGISTER < email | * > <password>
func registerHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
config := server.Config()
if !config.Accounts.Registration.Enabled {
rb.Add(nil, server.name, "FAIL", "REGISTER", "DISALLOWED", client.t("Account registration is disabled"))
return
}
if !client.registered && !config.Accounts.Registration.AllowBeforeConnect {
rb.Add(nil, server.name, "FAIL", "REGISTER", "DISALLOWED", client.t("You must complete the connection before registering your account"))
return
}
if client.registerCmdSent || client.Account() != "" {
rb.Add(nil, server.name, "FAIL", "REGISTER", "ALREADY_REGISTERED", client.t("You have already registered or attempted to register"))
return
}
accountName := client.Nick()
if accountName == "*" {
accountName = client.preregNick
}
if accountName == "" || accountName == "*" {
rb.Add(nil, server.name, "FAIL", "REGISTER", "INVALID_USERNAME", client.t("Username invalid or not given"))
return
}
callbackNamespace, callbackValue, err := parseCallback(msg.Params[0], config)
if err != nil {
rb.Add(nil, server.name, "FAIL", "REGISTER", "INVALID_EMAIL", client.t("A valid e-mail address is required"))
return
}
err = server.accounts.Register(client, accountName, callbackNamespace, callbackValue, msg.Params[1], rb.session.certfp)
switch err {
case nil:
if callbackNamespace == "*" {
err := server.accounts.Verify(client, accountName, "")
if err == nil {
if client.registered {
if !fixupNickEqualsAccount(client, rb, config) {
err = errNickAccountMismatch
}
}
if err == nil {
rb.Add(nil, server.name, "REGISTER", "SUCCESS", accountName, client.t("Account successfully registered"))
sendSuccessfulRegResponse(client, rb, true)
}
}
if err != nil {
server.logger.Error("internal", "accounts", "failed autoverification", accountName, err.Error())
rb.Add(nil, server.name, "FAIL", "REGISTER", "UNKNOWN_ERROR", client.t("An error occurred"))
}
} else {
rb.Add(nil, server.name, "REGISTER", "VERIFICATION_REQUIRED", accountName, fmt.Sprintf(client.t("Account created, pending verification; verification code has been sent to %s"), callbackValue))
client.registerCmdSent = true
}
case errAccountAlreadyRegistered, errAccountAlreadyUnregistered, errAccountMustHoldNick:
rb.Add(nil, server.name, "FAIL", "REGISTER", "USERNAME_EXISTS", client.t("Username is already registered or otherwise unavailable"))
case errAccountBadPassphrase:
rb.Add(nil, server.name, "FAIL", "REGISTER", "INVALID_PASSWORD", client.t("Password was invalid"))
case errCallbackFailed:
// TODO detect this in NS REGISTER as well
rb.Add(nil, server.name, "FAIL", "REGISTER", "UNACCEPTABLE_EMAIL", client.t("Could not dispatch verification e-mail"))
default:
rb.Add(nil, server.name, "FAIL", "REGISTER", "UNKNOWN_ERROR", client.t("Could not register"))
}
return
}
// VERIFY <account> <code>
func verifyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
config := server.Config()
if !config.Accounts.Registration.Enabled {
rb.Add(nil, server.name, "FAIL", "VERIFY", "DISALLOWED", client.t("Account registration is disabled"))
return
}
if !client.registered && !config.Accounts.Registration.AllowBeforeConnect {
rb.Add(nil, server.name, "FAIL", "VERIFY", "DISALLOWED", client.t("You must complete the connection before verifying your account"))
return
}
if client.Account() != "" {
rb.Add(nil, server.name, "FAIL", "VERIFY", "ALREADY_REGISTERED", client.t("You have already registered or attempted to register"))
return
}
accountName, verificationCode := msg.Params[0], msg.Params[1]
err := server.accounts.Verify(client, accountName, verificationCode)
if err == nil && client.registered {
if !fixupNickEqualsAccount(client, rb, config) {
err = errNickAccountMismatch
}
}
switch err {
case nil:
rb.Add(nil, server.name, "VERIFY", "SUCCESS", accountName, client.t("Account successfully registered"))
sendSuccessfulRegResponse(client, rb, true)
case errAccountVerificationInvalidCode:
rb.Add(nil, server.name, "FAIL", "VERIFY", "INVALID_CODE", client.t("Invalid verification code"))
default:
rb.Add(nil, server.name, "FAIL", "VERIFY", "UNKNOWN_ERROR", client.t("Failed to verify account"))
}
if err != nil && !client.registered {
// XXX pre-registration clients are exempt from fakelag;
// slow the client down to stop them spamming verify attempts
time.Sleep(time.Second)
}
return
}
// REHASH
func rehashHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
nick := client.Nick()

View File

@ -485,6 +485,11 @@ specs for more info: http://ircv3.net/specs/core/message-tags-3.3.html`,
text: `QUIT [reason]
Indicates that you're leaving the server, and shows everyone the given reason.`,
},
"register": {
text: `REGISTER <email | *> <password>
Registers an account in accordance with the draft/register capability.`,
},
"rehash": {
oper: true,
@ -544,6 +549,11 @@ The USERS command is not implemented.`,
text: `USERHOST <nickname>{ <nickname>}
Shows information about the given users. Takes up to 10 nicknames.`,
},
"verify": {
text: `VERIFY <account> <password>
Verifies an account in accordance with the draft/register capability.`,
},
"version": {
text: `VERSION [server]

View File

@ -33,7 +33,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
origNickMask := details.nickMask
isSanick := client != target
assignedNickname, err, back := client.server.clients.SetNick(target, session, nickname)
assignedNickname, err, back := client.server.clients.SetNick(target, session, nickname, false)
if err == errNicknameInUse {
if !isSanick {
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, details.nick, utils.SafeErrorParam(nickname), client.t("Nickname is already in use"))

View File

@ -853,24 +853,10 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
account = matches[1]
}
var callbackNamespace, callbackValue string
noneCallbackAllowed := false
for _, callback := range config.Accounts.Registration.EnabledCallbacks {
if callback == "*" {
noneCallbackAllowed = true
}
}
// XXX if ACC REGISTER allows registration with the `none` callback, then ignore
// any callback that was passed here (to avoid confusion in the case where the ircd
// has no mail server configured). otherwise, register using the provided callback:
if noneCallbackAllowed {
callbackNamespace = "*"
} else {
callbackNamespace, callbackValue = parseCallback(email, config.Accounts)
if callbackNamespace == "" || callbackValue == "" {
nsNotice(rb, client.t("Registration requires a valid e-mail address"))
return
}
callbackNamespace, callbackValue, validationErr := parseCallback(email, config)
if validationErr != nil {
nsNotice(rb, client.t("Registration requires a valid e-mail address"))
return
}
err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, rb.session.certfp)