3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-29 07:29:31 +01:00

Merge pull request #196 from slingamn/smtp.1

implement mailto callbacks
This commit is contained in:
Daniel Oaks 2018-02-23 07:53:36 -08:00 committed by GitHub
commit 0ea210c28c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 344 additions and 166 deletions

View File

@ -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,27 +572,16 @@ type rawClientAccount struct {
Name string
RegisteredAt string
Credentials string
Callback string
Verified bool
}
// LoginToAccount logs the client into the given account.
func (client *Client) LoginToAccount(account string) {
casefoldedAccount, err := CasefoldName(account)
if err != nil {
return
changed := client.SetAccountName(account)
if changed {
client.nickTimer.Touch()
}
if client.Account() == casefoldedAccount {
// already logged into this acct, no changing necessary
return
}
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 +595,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())
}
}

View File

@ -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

View File

@ -125,15 +125,21 @@ func (client *Client) AccountName() string {
return client.accountName
}
func (client *Client) SetAccountName(account string) {
func (client *Client) SetAccountName(account string) (changed bool) {
var casefoldedAccount string
var err error
if account != "" {
casefoldedAccount, _ = CasefoldName(account)
if casefoldedAccount, err = CasefoldName(account); err != nil {
return
}
}
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
changed = client.account != casefoldedAccount
client.account = casefoldedAccount
client.accountName = account
return
}
func (client *Client) HasMode(mode modes.Mode) bool {

View File

@ -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,34 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
return false
}
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
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
// 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 == "*" {
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 <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
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 +106,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 +172,69 @@ 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
}
// helper function to dispatch messages when a client successfully registers
func sendSuccessfulRegResponse(client *Client, rb *ResponseBuffer, forNS bool) {
if forNS {
rb.Notice(client.t("Account created"))
} else {
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()
if forNS {
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName()))
} else {
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"))
}
// 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 <accountname> <auth_code>
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 +363,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
return false
}
client.successfulSaslAuth(rb)
sendSuccessfulSaslAuth(client, rb, false)
return false
}
@ -329,7 +391,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
return false
}
client.successfulSaslAuth(rb)
sendSuccessfulSaslAuth(client, rb, false)
return false
}

View File

@ -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"))
}
}

View File

@ -152,6 +152,17 @@ accounts:
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