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:
commit
0ea210c28c
162
irc/accounts.go
162
irc/accounts.go
@ -4,27 +4,32 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
keyAccountExists = "account.exists %s"
|
||||
keyAccountVerified = "account.verified %s"
|
||||
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
|
||||
keyAccountRegTime = "account.registered.time %s"
|
||||
keyAccountCredentials = "account.credentials %s"
|
||||
keyCertToAccount = "account.creds.certfp %s"
|
||||
keyAccountExists = "account.exists %s"
|
||||
keyAccountVerified = "account.verified %s"
|
||||
keyAccountCallback = "account.callback %s"
|
||||
keyAccountVerificationCode = "account.verificationcode %s"
|
||||
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
|
||||
keyAccountRegTime = "account.registered.time %s"
|
||||
keyAccountCredentials = "account.credentials %s"
|
||||
keyCertToAccount = "account.creds.certfp %s"
|
||||
)
|
||||
|
||||
// everything about accounts is persistent; therefore, the database is the authoritative
|
||||
@ -51,7 +56,7 @@ func NewAccountManager(server *Server) *AccountManager {
|
||||
}
|
||||
|
||||
func (am *AccountManager) buildNickToAccountIndex() {
|
||||
if am.server.AccountConfig().NickReservation.Enabled {
|
||||
if !am.server.AccountConfig().NickReservation.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
@ -106,8 +111,10 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
|
||||
var creds AccountCredentials
|
||||
@ -134,6 +141,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
credStr := string(credText)
|
||||
|
||||
registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
|
||||
|
||||
var setOptions *buntdb.SetOptions
|
||||
ttl := am.server.AccountConfig().Registration.VerifyTimeout
|
||||
@ -159,6 +167,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
tx.Set(accountNameKey, account, setOptions)
|
||||
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
|
||||
tx.Set(credentialsKey, credStr, setOptions)
|
||||
tx.Set(callbackKey, callbackSpec, setOptions)
|
||||
if certfp != "" {
|
||||
tx.Set(certFPKey, casefoldedAccount, setOptions)
|
||||
}
|
||||
@ -169,7 +178,69 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue)
|
||||
if err != nil {
|
||||
am.Unregister(casefoldedAccount)
|
||||
return errCallbackFailed
|
||||
} else {
|
||||
return am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
_, _, err = tx.Set(verificationCodeKey, code, setOptions)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
|
||||
if callbackNamespace == "*" || callbackNamespace == "none" {
|
||||
return "", nil
|
||||
} else if callbackNamespace == "mailto" {
|
||||
return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue)
|
||||
} else {
|
||||
return "", errors.New(fmt.Sprintf("Callback not implemented: %s", callbackNamespace))
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
|
||||
config := am.server.AccountConfig().Registration.Callbacks.Mailto
|
||||
buf := make([]byte, 16)
|
||||
rand.Read(buf)
|
||||
code = hex.EncodeToString(buf)
|
||||
|
||||
subject := config.VerifyMessageSubject
|
||||
if subject == "" {
|
||||
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
|
||||
}
|
||||
messageStrings := []string{
|
||||
fmt.Sprintf("From: %s\r\n", config.Sender),
|
||||
fmt.Sprintf("To: %s\r\n", callbackValue),
|
||||
fmt.Sprintf("Subject: %s\r\n", subject),
|
||||
"\r\n", // end headers, begin message body
|
||||
fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n",
|
||||
fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
|
||||
"\r\n",
|
||||
client.t("To verify your account, issue one of these commands:") + "\r\n",
|
||||
fmt.Sprintf("/ACC VERIFY %s %s", casefoldedAccount, code) + "\r\n",
|
||||
fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
|
||||
}
|
||||
|
||||
var message []byte
|
||||
for i := 0; i < len(messageStrings); i++ {
|
||||
message = append(message, []byte(messageStrings[i])...)
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%d", config.Server, config.Port)
|
||||
var auth smtp.Auth
|
||||
if config.Username != "" && config.Password != "" {
|
||||
auth = smtp.PlainAuth("", config.Username, config.Password, config.Server)
|
||||
}
|
||||
|
||||
// TODO: this will never send the password in plaintext over a nonlocal link,
|
||||
// but it might send the email in plaintext, regardless of the value of
|
||||
// config.TLS.InsecureSkipVerify
|
||||
err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", fmt.Sprintf("Failed to dispatch e-mail: %v", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) Verify(client *Client, account string, code string) error {
|
||||
@ -182,6 +253,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
|
||||
var raw rawClientAccount
|
||||
@ -190,7 +263,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
am.serialCacheUpdateMutex.Lock()
|
||||
defer am.serialCacheUpdateMutex.Unlock()
|
||||
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
raw, err = am.loadRawAccount(tx, casefoldedAccount)
|
||||
if err == errAccountDoesNotExist {
|
||||
return errAccountDoesNotExist
|
||||
@ -200,15 +273,29 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
return errAccountAlreadyVerified
|
||||
}
|
||||
|
||||
// TODO add code verification here
|
||||
// return errAccountVerificationFailed if it fails
|
||||
// actually verify the code
|
||||
// a stored code of "" means a none callback / no code required
|
||||
success := false
|
||||
storedCode, err := tx.Get(verificationCodeKey)
|
||||
if err == nil {
|
||||
// this is probably unnecessary
|
||||
if storedCode == "" || subtle.ConstantTimeCompare([]byte(code), []byte(storedCode)) == 1 {
|
||||
success = true
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
return errAccountVerificationInvalidCode
|
||||
}
|
||||
|
||||
// verify the account
|
||||
tx.Set(verifiedKey, "1", nil)
|
||||
// don't need the code anymore
|
||||
tx.Delete(verificationCodeKey)
|
||||
// re-set all other keys, removing the TTL
|
||||
tx.Set(accountKey, "1", nil)
|
||||
tx.Set(accountNameKey, raw.Name, nil)
|
||||
tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
|
||||
tx.Set(callbackKey, raw.Callback, nil)
|
||||
tx.Set(credentialsKey, raw.Credentials, nil)
|
||||
|
||||
var creds AccountCredentials
|
||||
@ -295,6 +382,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
|
||||
_, e := tx.Get(accountKey)
|
||||
if e == buntdb.ErrNotFound {
|
||||
@ -302,15 +390,11 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
||||
return
|
||||
}
|
||||
|
||||
if result.Name, err = tx.Get(accountNameKey); err != nil {
|
||||
return
|
||||
}
|
||||
if result.RegisteredAt, err = tx.Get(registeredTimeKey); err != nil {
|
||||
return
|
||||
}
|
||||
if result.Credentials, err = tx.Get(credentialsKey); err != nil {
|
||||
return
|
||||
}
|
||||
result.Name, _ = tx.Get(accountNameKey)
|
||||
result.RegisteredAt, _ = tx.Get(registeredTimeKey)
|
||||
result.Credentials, _ = tx.Get(credentialsKey)
|
||||
result.Callback, _ = tx.Get(callbackKey)
|
||||
|
||||
if _, e = tx.Get(verifiedKey); e == nil {
|
||||
result.Verified = true
|
||||
}
|
||||
@ -328,6 +412,8 @@ func (am *AccountManager) Unregister(account string) error {
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
|
||||
var clients []*Client
|
||||
@ -343,6 +429,8 @@ func (am *AccountManager) Unregister(account string) error {
|
||||
tx.Delete(accountNameKey)
|
||||
tx.Delete(verifiedKey)
|
||||
tx.Delete(registeredTimeKey)
|
||||
tx.Delete(callbackKey)
|
||||
tx.Delete(verificationCodeKey)
|
||||
credText, err = tx.Get(credentialsKey)
|
||||
tx.Delete(credentialsKey)
|
||||
return nil
|
||||
@ -484,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())
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
138
irc/handlers.go
138
irc/handlers.go
@ -35,12 +35,18 @@ import (
|
||||
|
||||
// ACC [REGISTER|VERIFY] ...
|
||||
func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
// make sure reg is enabled
|
||||
if !server.AccountConfig().Registration.Enabled {
|
||||
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled"))
|
||||
return false
|
||||
}
|
||||
|
||||
subcommand := strings.ToLower(msg.Params[0])
|
||||
|
||||
if subcommand == "register" {
|
||||
return accRegisterHandler(server, client, msg, rb)
|
||||
} else if subcommand == "verify" {
|
||||
rb.Notice(client.t("VERIFY is not yet implemented"))
|
||||
return accVerifyHandler(server, client, msg, rb)
|
||||
} else {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
|
||||
}
|
||||
@ -48,14 +54,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
|
||||
}
|
||||
|
||||
|
147
irc/nickserv.go
147
irc/nickserv.go
@ -6,16 +6,20 @@ package irc
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
)
|
||||
|
||||
// TODO: "email" is an oversimplification here; it's actually any callback, e.g.,
|
||||
// person@example.com, mailto:person@example.com, tel:16505551234.
|
||||
const nickservHelp = `NickServ lets you register and log into a user account.
|
||||
|
||||
To register an account:
|
||||
/NS REGISTER username [password]
|
||||
/NS REGISTER username email [password]
|
||||
Leave out [password] if you're registering using your client certificate fingerprint.
|
||||
The server may or may not allow you to register anonymously (by sending * as your
|
||||
email address).
|
||||
|
||||
To verify an account (if you were sent a verification code):
|
||||
/NS VERIFY username code
|
||||
|
||||
To unregister an account:
|
||||
/NS UNREGISTER [username]
|
||||
@ -53,19 +57,14 @@ func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb
|
||||
}
|
||||
} else if command == "register" {
|
||||
// get params
|
||||
username, passphrase := extractParam(params)
|
||||
|
||||
// fail out if we need to
|
||||
if username == "" {
|
||||
rb.Notice(client.t("No username supplied"))
|
||||
return
|
||||
}
|
||||
|
||||
server.nickservRegisterHandler(client, username, passphrase, rb)
|
||||
username, afterUsername := extractParam(params)
|
||||
email, passphrase := extractParam(afterUsername)
|
||||
server.nickservRegisterHandler(client, username, email, passphrase, rb)
|
||||
} else if command == "verify" {
|
||||
username, code := extractParam(params)
|
||||
server.nickservVerifyHandler(client, username, code, rb)
|
||||
} else if command == "identify" {
|
||||
// get params
|
||||
username, passphrase := extractParam(params)
|
||||
|
||||
server.nickservIdentifyHandler(client, username, passphrase, rb)
|
||||
} else if command == "unregister" {
|
||||
username, _ := extractParam(params)
|
||||
@ -98,6 +97,10 @@ func (server *Server) nickservUnregisterHandler(client *Client, username string,
|
||||
return
|
||||
}
|
||||
|
||||
if cfname == client.Account() {
|
||||
client.server.accounts.Logout(client)
|
||||
}
|
||||
|
||||
err = server.accounts.Unregister(cfname)
|
||||
if err == errAccountDoesNotExist {
|
||||
rb.Notice(client.t(err.Error()))
|
||||
@ -108,18 +111,41 @@ func (server *Server) nickservUnregisterHandler(client *Client, username string,
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) nickservRegisterHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
|
||||
certfp := client.certfp
|
||||
if passphrase == "" && certfp == "" {
|
||||
rb.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert"))
|
||||
func (server *Server) nickservVerifyHandler(client *Client, username string, code string, rb *ResponseBuffer) {
|
||||
err := server.accounts.Verify(client, username, code)
|
||||
|
||||
var errorMessage string
|
||||
if err == errAccountVerificationInvalidCode || err == errAccountAlreadyVerified {
|
||||
errorMessage = err.Error()
|
||||
} else if err != nil {
|
||||
errorMessage = errAccountVerificationFailed.Error()
|
||||
}
|
||||
|
||||
if errorMessage != "" {
|
||||
rb.Notice(client.t(errorMessage))
|
||||
return
|
||||
}
|
||||
|
||||
sendSuccessfulRegResponse(client, rb, true)
|
||||
}
|
||||
|
||||
func (server *Server) nickservRegisterHandler(client *Client, username, email, passphrase string, rb *ResponseBuffer) {
|
||||
if !server.AccountConfig().Registration.Enabled {
|
||||
rb.Notice(client.t("Account registration has been disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
rb.Notice(client.t("No username supplied"))
|
||||
return
|
||||
}
|
||||
|
||||
certfp := client.certfp
|
||||
if passphrase == "" && certfp == "" {
|
||||
rb.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert"))
|
||||
return
|
||||
}
|
||||
|
||||
if client.LoggedIntoAccount() {
|
||||
if server.AccountConfig().Registration.AllowMultiplePerConnection {
|
||||
server.accounts.Logout(client)
|
||||
@ -129,26 +155,42 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
|
||||
}
|
||||
}
|
||||
|
||||
config := server.AccountConfig()
|
||||
var callbackNamespace, callbackValue string
|
||||
noneCallbackAllowed := false
|
||||
for _, callback := range(config.Registration.EnabledCallbacks) {
|
||||
if callback == "*" {
|
||||
noneCallbackAllowed = true
|
||||
}
|
||||
}
|
||||
// XXX if ACC REGISTER allows registration with the `none` callback, then ignore
|
||||
// any callback that was passed here (to avoid confusion in the case where the ircd
|
||||
// has no mail server configured). otherwise, register using the provided callback:
|
||||
if noneCallbackAllowed {
|
||||
callbackNamespace = "*"
|
||||
} else {
|
||||
callbackNamespace, callbackValue = parseCallback(email, config)
|
||||
if callbackNamespace == "" {
|
||||
rb.Notice(client.t("Registration requires a valid e-mail address"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// get and sanitise account name
|
||||
account := strings.TrimSpace(username)
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
|
||||
if err != nil || username == "*" {
|
||||
rb.Notice(client.t("Account name is not valid"))
|
||||
return
|
||||
}
|
||||
|
||||
// account could not be created and relevant numerics have been dispatched, abort
|
||||
if err != nil {
|
||||
if err != errAccountCreation {
|
||||
rb.Notice(client.t("Account registration failed"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = server.accounts.Register(client, account, "", "", passphrase, client.certfp)
|
||||
err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, client.certfp)
|
||||
if err == nil {
|
||||
err = server.accounts.Verify(client, casefoldedAccount, "")
|
||||
if callbackNamespace == "*" {
|
||||
err = server.accounts.Verify(client, account, "")
|
||||
if err == nil {
|
||||
sendSuccessfulRegResponse(client, rb, true)
|
||||
}
|
||||
} else {
|
||||
messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
|
||||
message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
|
||||
rb.Notice(message)
|
||||
}
|
||||
}
|
||||
|
||||
// details could not be stored and relevant numerics have been dispatched, abort
|
||||
@ -162,11 +204,6 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
|
||||
rb.Notice(client.t(errMsg))
|
||||
return
|
||||
}
|
||||
|
||||
rb.Notice(client.t("Account created"))
|
||||
rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount))
|
||||
rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
|
||||
server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), casefoldedAccount, client.nickMaskString))
|
||||
}
|
||||
|
||||
func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
|
||||
@ -176,31 +213,23 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra
|
||||
return
|
||||
}
|
||||
|
||||
loginSuccessful := false
|
||||
|
||||
// try passphrase
|
||||
if username != "" && passphrase != "" {
|
||||
// keep it the same as in the ACC CREATE stage
|
||||
accountName, err := CasefoldName(username)
|
||||
if err != nil {
|
||||
rb.Notice(client.t("Could not login with your username/password"))
|
||||
return
|
||||
}
|
||||
|
||||
err = server.accounts.AuthenticateByPassphrase(client, accountName, passphrase)
|
||||
if err == nil {
|
||||
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName))
|
||||
return
|
||||
}
|
||||
err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
|
||||
loginSuccessful = (err == nil)
|
||||
}
|
||||
|
||||
// try certfp
|
||||
if client.certfp != "" {
|
||||
if !loginSuccessful && client.certfp != "" {
|
||||
err := server.accounts.AuthenticateByCertFP(client)
|
||||
if err == nil {
|
||||
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName()))
|
||||
// TODO more notices?
|
||||
return
|
||||
}
|
||||
loginSuccessful = (err == nil)
|
||||
}
|
||||
|
||||
rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password"))
|
||||
if loginSuccessful {
|
||||
sendSuccessfulSaslAuth(client, rb, true)
|
||||
} else {
|
||||
rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password"))
|
||||
}
|
||||
}
|
||||
|
11
oragono.yaml
11
oragono.yaml
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user