diff --git a/irc/accounts.go b/irc/accounts.go index 6b8dfba4..70c0ce87 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -4,27 +4,32 @@ package irc import ( + "crypto/rand" + "crypto/subtle" + "encoding/hex" "encoding/json" + "errors" "fmt" + "net/smtp" "strconv" "strings" "sync" "time" - "github.com/goshuirc/irc-go/ircfmt" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/passwd" - "github.com/oragono/oragono/irc/sno" "github.com/tidwall/buntdb" ) const ( - keyAccountExists = "account.exists %s" - keyAccountVerified = "account.verified %s" - keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped - keyAccountRegTime = "account.registered.time %s" - keyAccountCredentials = "account.credentials %s" - keyCertToAccount = "account.creds.certfp %s" + keyAccountExists = "account.exists %s" + keyAccountVerified = "account.verified %s" + keyAccountCallback = "account.callback %s" + keyAccountVerificationCode = "account.verificationcode %s" + keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped + keyAccountRegTime = "account.registered.time %s" + keyAccountCredentials = "account.credentials %s" + keyCertToAccount = "account.creds.certfp %s" ) // everything about accounts is persistent; therefore, the database is the authoritative @@ -51,7 +56,7 @@ func NewAccountManager(server *Server) *AccountManager { } func (am *AccountManager) buildNickToAccountIndex() { - if am.server.AccountConfig().NickReservation.Enabled { + if !am.server.AccountConfig().NickReservation.Enabled { return } @@ -106,8 +111,10 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount) accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount) + callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount) registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) + verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount) certFPKey := fmt.Sprintf(keyCertToAccount, certfp) var creds AccountCredentials @@ -134,6 +141,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames credStr := string(credText) registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10) + callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue) var setOptions *buntdb.SetOptions ttl := am.server.AccountConfig().Registration.VerifyTimeout @@ -159,6 +167,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames tx.Set(accountNameKey, account, setOptions) tx.Set(registeredTimeKey, registeredTimeStr, setOptions) tx.Set(credentialsKey, credStr, setOptions) + tx.Set(callbackKey, callbackSpec, setOptions) if certfp != "" { tx.Set(certFPKey, casefoldedAccount, setOptions) } @@ -169,7 +178,69 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames return err } - return nil + code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue) + if err != nil { + am.Unregister(casefoldedAccount) + return errCallbackFailed + } else { + return am.server.store.Update(func(tx *buntdb.Tx) error { + _, _, err = tx.Set(verificationCodeKey, code, setOptions) + return err + }) + } +} + +func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) { + if callbackNamespace == "*" || callbackNamespace == "none" { + return "", nil + } else if callbackNamespace == "mailto" { + return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue) + } else { + return "", errors.New(fmt.Sprintf("Callback not implemented: %s", callbackNamespace)) + } +} + +func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) { + config := am.server.AccountConfig().Registration.Callbacks.Mailto + buf := make([]byte, 16) + rand.Read(buf) + code = hex.EncodeToString(buf) + + subject := config.VerifyMessageSubject + if subject == "" { + subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name) + } + messageStrings := []string{ + fmt.Sprintf("From: %s\r\n", config.Sender), + fmt.Sprintf("To: %s\r\n", callbackValue), + fmt.Sprintf("Subject: %s\r\n", subject), + "\r\n", // end headers, begin message body + fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n", + fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n", + "\r\n", + client.t("To verify your account, issue one of these commands:") + "\r\n", + fmt.Sprintf("/ACC VERIFY %s %s", casefoldedAccount, code) + "\r\n", + fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n", + } + + var message []byte + for i := 0; i < len(messageStrings); i++ { + message = append(message, []byte(messageStrings[i])...) + } + addr := fmt.Sprintf("%s:%d", config.Server, config.Port) + var auth smtp.Auth + if config.Username != "" && config.Password != "" { + auth = smtp.PlainAuth("", config.Username, config.Password, config.Server) + } + + // TODO: this will never send the password in plaintext over a nonlocal link, + // but it might send the email in plaintext, regardless of the value of + // config.TLS.InsecureSkipVerify + err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message) + if err != nil { + am.server.logger.Error("internal", fmt.Sprintf("Failed to dispatch e-mail: %v", err)) + } + return } func (am *AccountManager) Verify(client *Client, account string, code string) error { @@ -182,6 +253,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount) accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount) registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) + verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount) + callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount) credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) var raw rawClientAccount @@ -190,7 +263,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er am.serialCacheUpdateMutex.Lock() defer am.serialCacheUpdateMutex.Unlock() - am.server.store.Update(func(tx *buntdb.Tx) error { + err = am.server.store.Update(func(tx *buntdb.Tx) error { raw, err = am.loadRawAccount(tx, casefoldedAccount) if err == errAccountDoesNotExist { return errAccountDoesNotExist @@ -200,15 +273,29 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er return errAccountAlreadyVerified } - // TODO add code verification here - // return errAccountVerificationFailed if it fails + // actually verify the code + // a stored code of "" means a none callback / no code required + success := false + storedCode, err := tx.Get(verificationCodeKey) + if err == nil { + // this is probably unnecessary + if storedCode == "" || subtle.ConstantTimeCompare([]byte(code), []byte(storedCode)) == 1 { + success = true + } + } + if !success { + return errAccountVerificationInvalidCode + } // verify the account tx.Set(verifiedKey, "1", nil) + // don't need the code anymore + tx.Delete(verificationCodeKey) // re-set all other keys, removing the TTL tx.Set(accountKey, "1", nil) tx.Set(accountNameKey, raw.Name, nil) tx.Set(registeredTimeKey, raw.RegisteredAt, nil) + tx.Set(callbackKey, raw.Callback, nil) tx.Set(credentialsKey, raw.Credentials, nil) var creds AccountCredentials @@ -295,6 +382,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) + callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount) _, e := tx.Get(accountKey) if e == buntdb.ErrNotFound { @@ -302,15 +390,11 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string return } - if result.Name, err = tx.Get(accountNameKey); err != nil { - return - } - if result.RegisteredAt, err = tx.Get(registeredTimeKey); err != nil { - return - } - if result.Credentials, err = tx.Get(credentialsKey); err != nil { - return - } + result.Name, _ = tx.Get(accountNameKey) + result.RegisteredAt, _ = tx.Get(registeredTimeKey) + result.Credentials, _ = tx.Get(credentialsKey) + result.Callback, _ = tx.Get(callbackKey) + if _, e = tx.Get(verifiedKey); e == nil { result.Verified = true } @@ -328,6 +412,8 @@ func (am *AccountManager) Unregister(account string) error { accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount) registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) + callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount) + verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) var clients []*Client @@ -343,6 +429,8 @@ func (am *AccountManager) Unregister(account string) error { tx.Delete(accountNameKey) tx.Delete(verifiedKey) tx.Delete(registeredTimeKey) + tx.Delete(callbackKey) + tx.Delete(verificationCodeKey) credText, err = tx.Get(credentialsKey) tx.Delete(credentialsKey) return nil @@ -484,6 +572,7 @@ type rawClientAccount struct { Name string RegisteredAt string Credentials string + Callback string Verified bool } @@ -501,10 +590,6 @@ func (client *Client) LoginToAccount(account string) { client.SetAccountName(casefoldedAccount) client.nickTimer.Touch() - - client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]"), client.nickMaskString, casefoldedAccount)) - - //TODO(dan): This should output the AccountNotify message instead of the sasl accepted function below. } // LogoutOfAccount logs the client out of their current account. @@ -518,18 +603,9 @@ func (client *Client) LogoutOfAccount() { client.nickTimer.Touch() // dispatch account-notify + // TODO: doing the I/O here is kind of a kludge, let's move this somewhere else for friend := range client.Friends(caps.AccountNotify) { friend.Send(nil, client.nickMaskString, "ACCOUNT", "*") } } -// successfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages. -func (client *Client) successfulSaslAuth(rb *ResponseBuffer) { - rb.Add(nil, client.server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, client.AccountName(), fmt.Sprintf("You are now logged in as %s", client.AccountName())) - rb.Add(nil, client.server.name, RPL_SASLSUCCESS, client.nick, client.t("SASL authentication successful")) - - // dispatch account-notify - for friend := range client.Friends(caps.AccountNotify) { - friend.Send(nil, client.nickMaskString, "ACCOUNT", client.AccountName()) - } -} diff --git a/irc/errors.go b/irc/errors.go index d29bcc3a..4158480c 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -9,25 +9,27 @@ import "errors" // Runtime Errors var ( - errAccountAlreadyRegistered = errors.New("Account already exists") - errAccountCreation = errors.New("Account could not be created") - errAccountDoesNotExist = errors.New("Account does not exist") - errAccountVerificationFailed = errors.New("Account verification failed") - errAccountUnverified = errors.New("Account is not yet verified") - errAccountAlreadyVerified = errors.New("Account is already verified") - errAccountInvalidCredentials = errors.New("Invalid account credentials") - errCertfpAlreadyExists = errors.New("An account already exists with your certificate") - errChannelAlreadyRegistered = errors.New("Channel is already registered") - errChannelNameInUse = errors.New("Channel name in use") - errInvalidChannelName = errors.New("Invalid channel name") - errMonitorLimitExceeded = errors.New("Monitor limit exceeded") - errNickMissing = errors.New("nick missing") - errNicknameInUse = errors.New("nickname in use") - errNicknameReserved = errors.New("nickname is reserved") - errNoExistingBan = errors.New("Ban does not exist") - errNoSuchChannel = errors.New("No such channel") - errRenamePrivsNeeded = errors.New("Only chanops can rename channels") - errSaslFail = errors.New("SASL failed") + errAccountAlreadyRegistered = errors.New("Account already exists") + errAccountCreation = errors.New("Account could not be created") + errAccountDoesNotExist = errors.New("Account does not exist") + errAccountVerificationFailed = errors.New("Account verification failed") + errAccountVerificationInvalidCode = errors.New("Invalid account verification code") + errAccountUnverified = errors.New("Account is not yet verified") + errAccountAlreadyVerified = errors.New("Account is already verified") + errAccountInvalidCredentials = errors.New("Invalid account credentials") + errCallbackFailed = errors.New("Account verification could not be sent") + errCertfpAlreadyExists = errors.New("An account already exists with your certificate") + errChannelAlreadyRegistered = errors.New("Channel is already registered") + errChannelNameInUse = errors.New("Channel name in use") + errInvalidChannelName = errors.New("Invalid channel name") + errMonitorLimitExceeded = errors.New("Monitor limit exceeded") + errNickMissing = errors.New("nick missing") + errNicknameInUse = errors.New("nickname in use") + errNicknameReserved = errors.New("nickname is reserved") + errNoExistingBan = errors.New("Ban does not exist") + errNoSuchChannel = errors.New("No such channel") + errRenamePrivsNeeded = errors.New("Only chanops can rename channels") + errSaslFail = errors.New("SASL failed") ) // Socket Errors diff --git a/irc/handlers.go b/irc/handlers.go index 50538325..a86abca1 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -35,12 +35,18 @@ import ( // ACC [REGISTER|VERIFY] ... func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + // make sure reg is enabled + if !server.AccountConfig().Registration.Enabled { + rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled")) + return false + } + subcommand := strings.ToLower(msg.Params[0]) if subcommand == "register" { return accRegisterHandler(server, client, msg, rb) } else if subcommand == "verify" { - rb.Notice(client.t("VERIFY is not yet implemented")) + return accVerifyHandler(server, client, msg, rb) } else { rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand")) } @@ -48,14 +54,33 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo return false } -// ACC REGISTER [callback_namespace:] [cred_type] : -func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - // make sure reg is enabled - if !server.AccountConfig().Registration.Enabled { - rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled")) - return false +func parseCallback(spec string, config *AccountConfig) (callbackNamespace string, callbackValue string) { + callback := strings.ToLower(spec) + if callback == "*" { + callbackNamespace = "*" + } else if strings.Contains(callback, ":") { + callbackValues := strings.SplitN(callback, ":", 2) + callbackNamespace, callbackValue = callbackValues[0], callbackValues[1] + } else { + // "the IRC server MAY choose to use mailto as a default" + 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 + } + } + // error value + callbackNamespace = "" + return +} + +// ACC REGISTER [callback_namespace:] [cred_type] : +func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { // clients can't reg new accounts if they're already logged in if client.LoggedIntoAccount() { if server.AccountConfig().Registration.AllowMultiplePerConnection { @@ -80,30 +105,11 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r return false } - callback := strings.ToLower(msg.Params[2]) - var callbackNamespace, callbackValue string + callbackSpec := msg.Params[2] + callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig()) - if callback == "*" { - callbackNamespace = "*" - } else if strings.Contains(callback, ":") { - callbackValues := strings.SplitN(callback, ":", 2) - callbackNamespace, callbackValue = callbackValues[0], callbackValues[1] - } else { - callbackNamespace = server.AccountConfig().Registration.EnabledCallbacks[0] - callbackValue = callback - } - - // ensure the callback namespace is valid - // need to search callback list, maybe look at using a map later? - var callbackValid bool - for _, name := range server.AccountConfig().Registration.EnabledCallbacks { - if callbackNamespace == name { - callbackValid = true - } - } - - if !callbackValid { - rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackNamespace, client.t("Callback namespace is not supported")) + if callbackNamespace == "" { + rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackSpec, client.t("Callback namespace is not supported")) return false } @@ -165,14 +171,66 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r if err != nil { return false } - client.Send(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, casefoldedAccount, client.t("Account created")) - client.Send(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount)) - client.Send(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful")) - server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), casefoldedAccount, client.nickMaskString)) + sendSuccessfulRegResponse(client, rb, false) + } else { + messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s") + message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue) + rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, client.nick, casefoldedAccount, message) } - // dispatch callback - rb.Notice(fmt.Sprintf("We should dispatch a real callback here to %s:%s", callbackNamespace, callbackValue)) + return false +} + +func sendSuccessfulRegResponse(client *Client, rb *ResponseBuffer, forNS bool) { + if forNS { + rb.Notice(client.t("Account created")) + } + rb.Add(nil, client.server.name, RPL_REGISTRATION_SUCCESS, client.nick, client.AccountName(), client.t("Account created")) + sendSuccessfulSaslAuth(client, rb, forNS) +} + +// sendSuccessfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages. +func sendSuccessfulSaslAuth(client *Client, rb *ResponseBuffer, forNS bool) { + account := client.AccountName() + rb.Add(nil, client.server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account, fmt.Sprintf("You are now logged in as %s", account)) + rb.Add(nil, client.server.name, RPL_SASLSUCCESS, client.nick, client.t("SASL authentication successful")) + + if forNS { + rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName())) + } + + // dispatch account-notify + for friend := range client.Friends(caps.AccountNotify) { + friend.Send(nil, client.nickMaskString, "ACCOUNT", account) + } + + client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]"), client.nickMaskString, account)) +} + +// ACC VERIFY +func accVerifyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + account := strings.TrimSpace(msg.Params[1]) + err := server.accounts.Verify(client, account, msg.Params[2]) + + var code string + var message string + + if err == errAccountVerificationInvalidCode { + code = ERR_ACCOUNT_INVALID_VERIFY_CODE + message = err.Error() + } else if err == errAccountAlreadyVerified { + code = ERR_ACCOUNT_ALREADY_VERIFIED + message = err.Error() + } else if err != nil { + code = ERR_UNKNOWNERROR + message = errAccountVerificationFailed.Error() + } + + if err == nil { + sendSuccessfulRegResponse(client, rb, false) + } else { + rb.Add(nil, server.name, code, client.nick, account, client.t(message)) + } return false } @@ -301,7 +359,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value [] return false } - client.successfulSaslAuth(rb) + sendSuccessfulSaslAuth(client, rb, false) return false } @@ -329,7 +387,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value return false } - client.successfulSaslAuth(rb) + sendSuccessfulSaslAuth(client, rb, false) return false } diff --git a/irc/nickserv.go b/irc/nickserv.go index ddba5432..d2a20b9d 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -6,16 +6,20 @@ package irc import ( "fmt" "strings" - - "github.com/goshuirc/irc-go/ircfmt" - "github.com/oragono/oragono/irc/sno" ) +// TODO: "email" is an oversimplification here; it's actually any callback, e.g., +// person@example.com, mailto:person@example.com, tel:16505551234. const nickservHelp = `NickServ lets you register and log into a user account. To register an account: - /NS REGISTER username [password] + /NS REGISTER username email [password] Leave out [password] if you're registering using your client certificate fingerprint. +The server may or may not allow you to register anonymously (by sending * as your +email address). + +To verify an account (if you were sent a verification code): + /NS VERIFY username code To unregister an account: /NS UNREGISTER [username] @@ -53,19 +57,14 @@ func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb } } else if command == "register" { // get params - username, passphrase := extractParam(params) - - // fail out if we need to - if username == "" { - rb.Notice(client.t("No username supplied")) - return - } - - server.nickservRegisterHandler(client, username, passphrase, rb) + username, afterUsername := extractParam(params) + email, passphrase := extractParam(afterUsername) + server.nickservRegisterHandler(client, username, email, passphrase, rb) + } else if command == "verify" { + username, code := extractParam(params) + server.nickservVerifyHandler(client, username, code, rb) } else if command == "identify" { - // get params username, passphrase := extractParam(params) - server.nickservIdentifyHandler(client, username, passphrase, rb) } else if command == "unregister" { username, _ := extractParam(params) @@ -98,6 +97,10 @@ func (server *Server) nickservUnregisterHandler(client *Client, username string, return } + if cfname == client.Account() { + client.server.accounts.Logout(client) + } + err = server.accounts.Unregister(cfname) if err == errAccountDoesNotExist { rb.Notice(client.t(err.Error())) @@ -108,18 +111,41 @@ func (server *Server) nickservUnregisterHandler(client *Client, username string, } } -func (server *Server) nickservRegisterHandler(client *Client, username, passphrase string, rb *ResponseBuffer) { - certfp := client.certfp - if passphrase == "" && certfp == "" { - rb.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert")) +func (server *Server) nickservVerifyHandler(client *Client, username string, code string, rb *ResponseBuffer) { + 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 errorMessage != "" { + rb.Notice(client.t(errorMessage)) return } + sendSuccessfulRegResponse(client, rb, true) +} + +func (server *Server) nickservRegisterHandler(client *Client, username, email, passphrase string, rb *ResponseBuffer) { if !server.AccountConfig().Registration.Enabled { rb.Notice(client.t("Account registration has been disabled")) return } + if username == "" { + rb.Notice(client.t("No username supplied")) + return + } + + certfp := client.certfp + if passphrase == "" && certfp == "" { + rb.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert")) + return + } + if client.LoggedIntoAccount() { if server.AccountConfig().Registration.AllowMultiplePerConnection { server.accounts.Logout(client) @@ -129,26 +155,42 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra } } + config := server.AccountConfig() + var callbackNamespace, callbackValue string + noneCallbackAllowed := false + for _, callback := range(config.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) + if callbackNamespace == "" { + rb.Notice(client.t("Registration requires a valid e-mail address")) + return + } + } + // get and sanitise account name account := strings.TrimSpace(username) - casefoldedAccount, err := CasefoldName(account) - // probably don't need explicit check for "*" here... but let's do it anyway just to make sure - if err != nil || username == "*" { - rb.Notice(client.t("Account name is not valid")) - return - } - // account could not be created and relevant numerics have been dispatched, abort - if err != nil { - if err != errAccountCreation { - rb.Notice(client.t("Account registration failed")) - } - return - } - - err = server.accounts.Register(client, account, "", "", passphrase, client.certfp) + err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, client.certfp) if err == nil { - err = server.accounts.Verify(client, casefoldedAccount, "") + if callbackNamespace == "*" { + err = server.accounts.Verify(client, account, "") + if err == nil { + sendSuccessfulRegResponse(client, rb, true) + } + } else { + messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s") + message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue) + rb.Notice(message) + } } // details could not be stored and relevant numerics have been dispatched, abort @@ -162,11 +204,6 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra rb.Notice(client.t(errMsg)) return } - - rb.Notice(client.t("Account created")) - rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount)) - rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful")) - server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), casefoldedAccount, client.nickMaskString)) } func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) { @@ -176,31 +213,23 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra return } + loginSuccessful := false + // try passphrase if username != "" && passphrase != "" { - // keep it the same as in the ACC CREATE stage - accountName, err := CasefoldName(username) - if err != nil { - rb.Notice(client.t("Could not login with your username/password")) - return - } - - err = server.accounts.AuthenticateByPassphrase(client, accountName, passphrase) - if err == nil { - rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName)) - return - } + err := server.accounts.AuthenticateByPassphrase(client, username, passphrase) + loginSuccessful = (err == nil) } // try certfp - if client.certfp != "" { + if !loginSuccessful && client.certfp != "" { err := server.accounts.AuthenticateByCertFP(client) - if err == nil { - rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName())) - // TODO more notices? - return - } + loginSuccessful = (err == nil) } - rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password")) + if loginSuccessful { + sendSuccessfulSaslAuth(client, rb, true) + } else { + rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password")) + } } diff --git a/oragono.yaml b/oragono.yaml index cc68e87d..d32a50db 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -151,7 +151,18 @@ accounts: # callbacks to allow enabled-callbacks: - none # no verification needed, will instantly register successfully - + + # example configuration for sending verification emails via a local mail relay + # callbacks: + # mailto: + # server: localhost + # port: 25 + # tls: + # enabled: false + # username: "" + # password: "" + # sender: "admin@my.network" + # allow multiple account registrations per connection # this is for testing purposes and shouldn't be allowed on real networks allow-multiple-per-connection: false