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,23 +4,28 @@
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"
keyAccountCallback = "account.callback %s"
keyAccountVerificationCode = "account.verificationcode %s"
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
keyAccountRegTime = "account.registered.time %s" keyAccountRegTime = "account.registered.time %s"
keyAccountCredentials = "account.credentials %s" keyAccountCredentials = "account.credentials %s"
@ -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,27 +572,16 @@ type rawClientAccount struct {
Name string Name string
RegisteredAt string RegisteredAt string
Credentials string Credentials string
Callback string
Verified bool Verified bool
} }
// LoginToAccount logs the client into the given account. // LoginToAccount logs the client into the given account.
func (client *Client) LoginToAccount(account string) { func (client *Client) LoginToAccount(account string) {
casefoldedAccount, err := CasefoldName(account) changed := client.SetAccountName(account)
if err != nil { if changed {
return
}
if client.Account() == casefoldedAccount {
// already logged into this acct, no changing necessary
return
}
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 +595,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

@ -13,9 +13,11 @@ var (
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")
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
errAccountUnverified = errors.New("Account is not yet verified") errAccountUnverified = errors.New("Account is not yet verified")
errAccountAlreadyVerified = errors.New("Account is already verified") errAccountAlreadyVerified = errors.New("Account is already verified")
errAccountInvalidCredentials = errors.New("Invalid account credentials") 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") errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
errChannelAlreadyRegistered = errors.New("Channel is already registered") errChannelAlreadyRegistered = errors.New("Channel is already registered")
errChannelNameInUse = errors.New("Channel name in use") errChannelNameInUse = errors.New("Channel name in use")

View File

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

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,34 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
return false return false
} }
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential> // helper function to parse ACC callbacks, e.g., mailto:person@example.com, tel:16505551234
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func parseCallback(spec string, config *AccountConfig) (callbackNamespace string, callbackValue string) {
// make sure reg is enabled callback := strings.ToLower(spec)
if !server.AccountConfig().Registration.Enabled { if callback == "*" {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled")) callbackNamespace = "*"
return false } 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 // 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 +106,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 +172,69 @@ 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)) }
// 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 return false
} }
@ -301,7 +363,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 +391,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
}
} }
if loginSuccessful {
sendSuccessfulSaslAuth(client, rb, true)
} else {
rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password")) rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password"))
}
} }

View File

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