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

implement mailto callbacks

This commit is contained in:
Shivaram Lingamneni 2018-02-20 04:20:30 -05:00
parent 25f8b15232
commit 89ae261739
5 changed files with 329 additions and 153 deletions

View File

@ -4,27 +4,32 @@
package irc package irc
import ( import (
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/smtp"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/caps"
"github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/passwd"
"github.com/oragono/oragono/irc/sno"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
) )
const ( const (
keyAccountExists = "account.exists %s" keyAccountExists = "account.exists %s"
keyAccountVerified = "account.verified %s" keyAccountVerified = "account.verified %s"
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped keyAccountCallback = "account.callback %s"
keyAccountRegTime = "account.registered.time %s" keyAccountVerificationCode = "account.verificationcode %s"
keyAccountCredentials = "account.credentials %s" keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
keyCertToAccount = "account.creds.certfp %s" 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 // everything about accounts is persistent; therefore, the database is the authoritative
@ -51,7 +56,7 @@ func NewAccountManager(server *Server) *AccountManager {
} }
func (am *AccountManager) buildNickToAccountIndex() { func (am *AccountManager) buildNickToAccountIndex() {
if am.server.AccountConfig().NickReservation.Enabled { if !am.server.AccountConfig().NickReservation.Enabled {
return return
} }
@ -106,8 +111,10 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount) accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount) accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
certFPKey := fmt.Sprintf(keyCertToAccount, certfp) certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
var creds AccountCredentials var creds AccountCredentials
@ -134,6 +141,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
credStr := string(credText) credStr := string(credText)
registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10) registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
var setOptions *buntdb.SetOptions var setOptions *buntdb.SetOptions
ttl := am.server.AccountConfig().Registration.VerifyTimeout 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(accountNameKey, account, setOptions)
tx.Set(registeredTimeKey, registeredTimeStr, setOptions) tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
tx.Set(credentialsKey, credStr, setOptions) tx.Set(credentialsKey, credStr, setOptions)
tx.Set(callbackKey, callbackSpec, setOptions)
if certfp != "" { if certfp != "" {
tx.Set(certFPKey, casefoldedAccount, setOptions) tx.Set(certFPKey, casefoldedAccount, setOptions)
} }
@ -169,7 +178,69 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
return err 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 { 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) accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount) accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
var raw rawClientAccount var raw rawClientAccount
@ -190,7 +263,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
am.serialCacheUpdateMutex.Lock() am.serialCacheUpdateMutex.Lock()
defer am.serialCacheUpdateMutex.Unlock() 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) raw, err = am.loadRawAccount(tx, casefoldedAccount)
if err == errAccountDoesNotExist { if err == errAccountDoesNotExist {
return errAccountDoesNotExist return errAccountDoesNotExist
@ -200,15 +273,29 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
return errAccountAlreadyVerified return errAccountAlreadyVerified
} }
// TODO add code verification here // actually verify the code
// return errAccountVerificationFailed if it fails // 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 // verify the account
tx.Set(verifiedKey, "1", nil) tx.Set(verifiedKey, "1", nil)
// don't need the code anymore
tx.Delete(verificationCodeKey)
// re-set all other keys, removing the TTL // re-set all other keys, removing the TTL
tx.Set(accountKey, "1", nil) tx.Set(accountKey, "1", nil)
tx.Set(accountNameKey, raw.Name, nil) tx.Set(accountNameKey, raw.Name, nil)
tx.Set(registeredTimeKey, raw.RegisteredAt, nil) tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
tx.Set(callbackKey, raw.Callback, nil)
tx.Set(credentialsKey, raw.Credentials, nil) tx.Set(credentialsKey, raw.Credentials, nil)
var creds AccountCredentials var creds AccountCredentials
@ -295,6 +382,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
_, e := tx.Get(accountKey) _, e := tx.Get(accountKey)
if e == buntdb.ErrNotFound { if e == buntdb.ErrNotFound {
@ -302,15 +390,11 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
return return
} }
if result.Name, err = tx.Get(accountNameKey); err != nil { result.Name, _ = tx.Get(accountNameKey)
return result.RegisteredAt, _ = tx.Get(registeredTimeKey)
} result.Credentials, _ = tx.Get(credentialsKey)
if result.RegisteredAt, err = tx.Get(registeredTimeKey); err != nil { result.Callback, _ = tx.Get(callbackKey)
return
}
if result.Credentials, err = tx.Get(credentialsKey); err != nil {
return
}
if _, e = tx.Get(verifiedKey); e == nil { if _, e = tx.Get(verifiedKey); e == nil {
result.Verified = true result.Verified = true
} }
@ -328,6 +412,8 @@ func (am *AccountManager) Unregister(account string) error {
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount) accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount) registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount) credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
var clients []*Client var clients []*Client
@ -343,6 +429,8 @@ func (am *AccountManager) Unregister(account string) error {
tx.Delete(accountNameKey) tx.Delete(accountNameKey)
tx.Delete(verifiedKey) tx.Delete(verifiedKey)
tx.Delete(registeredTimeKey) tx.Delete(registeredTimeKey)
tx.Delete(callbackKey)
tx.Delete(verificationCodeKey)
credText, err = tx.Get(credentialsKey) credText, err = tx.Get(credentialsKey)
tx.Delete(credentialsKey) tx.Delete(credentialsKey)
return nil return nil
@ -484,6 +572,7 @@ type rawClientAccount struct {
Name string Name string
RegisteredAt string RegisteredAt string
Credentials string Credentials string
Callback string
Verified bool Verified bool
} }
@ -501,10 +590,6 @@ func (client *Client) LoginToAccount(account string) {
client.SetAccountName(casefoldedAccount) client.SetAccountName(casefoldedAccount)
client.nickTimer.Touch() 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. // LogoutOfAccount logs the client out of their current account.
@ -518,18 +603,9 @@ func (client *Client) LogoutOfAccount() {
client.nickTimer.Touch() client.nickTimer.Touch()
// dispatch account-notify // 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) { for friend := range client.Friends(caps.AccountNotify) {
friend.Send(nil, client.nickMaskString, "ACCOUNT", "*") 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 // Runtime Errors
var ( var (
errAccountAlreadyRegistered = errors.New("Account already exists") errAccountAlreadyRegistered = errors.New("Account already exists")
errAccountCreation = errors.New("Account could not be created") errAccountCreation = errors.New("Account could not be created")
errAccountDoesNotExist = errors.New("Account does not exist") errAccountDoesNotExist = errors.New("Account does not exist")
errAccountVerificationFailed = errors.New("Account verification failed") errAccountVerificationFailed = errors.New("Account verification failed")
errAccountUnverified = errors.New("Account is not yet verified") errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
errAccountAlreadyVerified = errors.New("Account is already verified") errAccountUnverified = errors.New("Account is not yet verified")
errAccountInvalidCredentials = errors.New("Invalid account credentials") errAccountAlreadyVerified = errors.New("Account is already verified")
errCertfpAlreadyExists = errors.New("An account already exists with your certificate") errAccountInvalidCredentials = errors.New("Invalid account credentials")
errChannelAlreadyRegistered = errors.New("Channel is already registered") errCallbackFailed = errors.New("Account verification could not be sent")
errChannelNameInUse = errors.New("Channel name in use") errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
errInvalidChannelName = errors.New("Invalid channel name") errChannelAlreadyRegistered = errors.New("Channel is already registered")
errMonitorLimitExceeded = errors.New("Monitor limit exceeded") errChannelNameInUse = errors.New("Channel name in use")
errNickMissing = errors.New("nick missing") errInvalidChannelName = errors.New("Invalid channel name")
errNicknameInUse = errors.New("nickname in use") errMonitorLimitExceeded = errors.New("Monitor limit exceeded")
errNicknameReserved = errors.New("nickname is reserved") errNickMissing = errors.New("nick missing")
errNoExistingBan = errors.New("Ban does not exist") errNicknameInUse = errors.New("nickname in use")
errNoSuchChannel = errors.New("No such channel") errNicknameReserved = errors.New("nickname is reserved")
errRenamePrivsNeeded = errors.New("Only chanops can rename channels") errNoExistingBan = errors.New("Ban does not exist")
errSaslFail = errors.New("SASL failed") errNoSuchChannel = errors.New("No such channel")
errRenamePrivsNeeded = errors.New("Only chanops can rename channels")
errSaslFail = errors.New("SASL failed")
) )
// Socket Errors // Socket Errors

View File

@ -35,12 +35,18 @@ import (
// ACC [REGISTER|VERIFY] ... // ACC [REGISTER|VERIFY] ...
func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { 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]) subcommand := strings.ToLower(msg.Params[0])
if subcommand == "register" { if subcommand == "register" {
return accRegisterHandler(server, client, msg, rb) return accRegisterHandler(server, client, msg, rb)
} else if subcommand == "verify" { } else if subcommand == "verify" {
rb.Notice(client.t("VERIFY is not yet implemented")) return accVerifyHandler(server, client, msg, rb)
} else { } else {
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand")) 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 return false
} }
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential> func parseCallback(spec string, config *AccountConfig) (callbackNamespace string, callbackValue string) {
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { callback := strings.ToLower(spec)
// make sure reg is enabled if callback == "*" {
if !server.AccountConfig().Registration.Enabled { callbackNamespace = "*"
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled")) } else if strings.Contains(callback, ":") {
return false 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 // clients can't reg new accounts if they're already logged in
if client.LoggedIntoAccount() { if client.LoggedIntoAccount() {
if server.AccountConfig().Registration.AllowMultiplePerConnection { if server.AccountConfig().Registration.AllowMultiplePerConnection {
@ -80,30 +105,11 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
return false return false
} }
callback := strings.ToLower(msg.Params[2]) callbackSpec := msg.Params[2]
var callbackNamespace, callbackValue string callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
if callback == "*" { if callbackNamespace == "" {
callbackNamespace = "*" rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackSpec, client.t("Callback namespace is not supported"))
} 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"))
return false return false
} }
@ -165,14 +171,66 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
if err != nil { if err != nil {
return false return false
} }
client.Send(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, casefoldedAccount, client.t("Account created")) sendSuccessfulRegResponse(client, rb, false)
client.Send(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount)) } else {
client.Send(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful")) messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
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)) message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, client.nick, casefoldedAccount, message)
} }
// dispatch callback return false
rb.Notice(fmt.Sprintf("We should dispatch a real callback here to %s:%s", callbackNamespace, callbackValue)) }
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 <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 return false
} }
@ -301,7 +359,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
return false return false
} }
client.successfulSaslAuth(rb) sendSuccessfulSaslAuth(client, rb, false)
return false return false
} }
@ -329,7 +387,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
return false return false
} }
client.successfulSaslAuth(rb) sendSuccessfulSaslAuth(client, rb, false)
return false return false
} }

View File

@ -6,16 +6,20 @@ package irc
import ( import (
"fmt" "fmt"
"strings" "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. const nickservHelp = `NickServ lets you register and log into a user account.
To register an 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. 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: To unregister an account:
/NS UNREGISTER [username] /NS UNREGISTER [username]
@ -53,19 +57,14 @@ func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb
} }
} else if command == "register" { } else if command == "register" {
// get params // get params
username, passphrase := extractParam(params) username, afterUsername := extractParam(params)
email, passphrase := extractParam(afterUsername)
// fail out if we need to server.nickservRegisterHandler(client, username, email, passphrase, rb)
if username == "" { } else if command == "verify" {
rb.Notice(client.t("No username supplied")) username, code := extractParam(params)
return server.nickservVerifyHandler(client, username, code, rb)
}
server.nickservRegisterHandler(client, username, passphrase, rb)
} else if command == "identify" { } else if command == "identify" {
// get params
username, passphrase := extractParam(params) username, passphrase := extractParam(params)
server.nickservIdentifyHandler(client, username, passphrase, rb) server.nickservIdentifyHandler(client, username, passphrase, rb)
} else if command == "unregister" { } else if command == "unregister" {
username, _ := extractParam(params) username, _ := extractParam(params)
@ -98,6 +97,10 @@ func (server *Server) nickservUnregisterHandler(client *Client, username string,
return return
} }
if cfname == client.Account() {
client.server.accounts.Logout(client)
}
err = server.accounts.Unregister(cfname) err = server.accounts.Unregister(cfname)
if err == errAccountDoesNotExist { if err == errAccountDoesNotExist {
rb.Notice(client.t(err.Error())) 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) { func (server *Server) nickservVerifyHandler(client *Client, username string, code string, rb *ResponseBuffer) {
certfp := client.certfp err := server.accounts.Verify(client, username, code)
if passphrase == "" && certfp == "" {
rb.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert")) 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 return
} }
sendSuccessfulRegResponse(client, rb, true)
}
func (server *Server) nickservRegisterHandler(client *Client, username, email, passphrase string, rb *ResponseBuffer) {
if !server.AccountConfig().Registration.Enabled { if !server.AccountConfig().Registration.Enabled {
rb.Notice(client.t("Account registration has been disabled")) rb.Notice(client.t("Account registration has been disabled"))
return 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 client.LoggedIntoAccount() {
if server.AccountConfig().Registration.AllowMultiplePerConnection { if server.AccountConfig().Registration.AllowMultiplePerConnection {
server.accounts.Logout(client) 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 // get and sanitise account name
account := strings.TrimSpace(username) 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 err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, client.certfp)
if err != nil {
if err != errAccountCreation {
rb.Notice(client.t("Account registration failed"))
}
return
}
err = server.accounts.Register(client, account, "", "", passphrase, client.certfp)
if err == nil { 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 // 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)) rb.Notice(client.t(errMsg))
return 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) { func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
@ -176,31 +213,23 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra
return return
} }
loginSuccessful := false
// try passphrase // try passphrase
if username != "" && passphrase != "" { if username != "" && passphrase != "" {
// keep it the same as in the ACC CREATE stage err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
accountName, err := CasefoldName(username) loginSuccessful = (err == nil)
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
}
} }
// try certfp // try certfp
if client.certfp != "" { if !loginSuccessful && client.certfp != "" {
err := server.accounts.AuthenticateByCertFP(client) err := server.accounts.AuthenticateByCertFP(client)
if err == nil { loginSuccessful = (err == nil)
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName()))
// TODO more notices?
return
}
} }
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

@ -151,7 +151,18 @@ accounts:
# callbacks to allow # callbacks to allow
enabled-callbacks: enabled-callbacks:
- none # no verification needed, will instantly register successfully - 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 # allow multiple account registrations per connection
# this is for testing purposes and shouldn't be allowed on real networks # this is for testing purposes and shouldn't be allowed on real networks
allow-multiple-per-connection: false allow-multiple-per-connection: false