mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-22 03:49:27 +01:00
refactor account registration, add nick enforcement
This commit is contained in:
parent
fcd0a75469
commit
ad73d68807
@ -98,7 +98,7 @@ In consequence, there is a lot of state (in particular, server and channel state
|
||||
|
||||
There are some mutexes that are "tier 0": anything in a subpackage of `irc` (e.g., `irc/logger` or `irc/connection_limits`) shouldn't acquire mutexes defined in `irc`.
|
||||
|
||||
We are using `buntdb` for persistence; a `buntdb.DB` has an `RWMutex` inside it, with read-write transactions getting the `Lock()` and read-only transactions getting the `RLock()`. We haven't completely decided where this lock fits into the overall lock model. For now, it's probably better to err on the side of caution: if possible, don't acquire new locks inside the `buntdb` transaction, and be careful about what locks are held around the transaction as well.
|
||||
We are using `buntdb` for persistence; a `buntdb.DB` has an `RWMutex` inside it, with read-write transactions getting the `Lock()` and read-only transactions getting the `RLock()`. This mutex is considered tier 1. However, it's shared globally across all consumers, so if possible you should avoid acquiring it while holding ordinary application-level mutexes.
|
||||
|
||||
## Command handlers and ResponseBuffer
|
||||
|
||||
|
@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
// AccountRegistration manages the registration of accounts.
|
||||
type AccountRegistration struct {
|
||||
Enabled bool
|
||||
EnabledCallbacks []string
|
||||
EnabledCredentialTypes []string
|
||||
AllowMultiplePerConnection bool
|
||||
}
|
||||
|
||||
// AccountCredentials stores the various methods for verifying accounts.
|
||||
type AccountCredentials struct {
|
||||
PassphraseSalt []byte
|
||||
PassphraseHash []byte
|
||||
Certificate string // fingerprint
|
||||
}
|
||||
|
||||
// NewAccountRegistration returns a new AccountRegistration, configured correctly.
|
||||
func NewAccountRegistration(config AccountRegistrationConfig) (accountReg AccountRegistration) {
|
||||
if config.Enabled {
|
||||
accountReg.Enabled = true
|
||||
accountReg.AllowMultiplePerConnection = config.AllowMultiplePerConnection
|
||||
for _, name := range config.EnabledCallbacks {
|
||||
// we store "none" as "*" internally
|
||||
if name == "none" {
|
||||
name = "*"
|
||||
}
|
||||
accountReg.EnabledCallbacks = append(accountReg.EnabledCallbacks, name)
|
||||
}
|
||||
// no need to make this configurable, right now at least
|
||||
accountReg.EnabledCredentialTypes = []string{
|
||||
"passphrase",
|
||||
"certfp",
|
||||
}
|
||||
}
|
||||
return accountReg
|
||||
}
|
||||
|
||||
// removeFailedAccRegisterData removes the data created by ACC REGISTER if the account creation fails early.
|
||||
func removeFailedAccRegisterData(store *buntdb.DB, account string) {
|
||||
// error is ignored here, we can't do much about it anyways
|
||||
store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Delete(fmt.Sprintf(keyAccountExists, account))
|
||||
tx.Delete(fmt.Sprintf(keyAccountRegTime, account))
|
||||
tx.Delete(fmt.Sprintf(keyAccountCredentials, account))
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
517
irc/accounts.go
517
irc/accounts.go
@ -7,10 +7,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
@ -24,6 +27,423 @@ const (
|
||||
keyCertToAccount = "account.creds.certfp %s"
|
||||
)
|
||||
|
||||
// everything about accounts is persistent; therefore, the database is the authoritative
|
||||
// source of truth for all account information. anything on the heap is just a cache
|
||||
type AccountManager struct {
|
||||
sync.RWMutex // tier 2
|
||||
serialCacheUpdateMutex sync.Mutex // tier 3
|
||||
|
||||
server *Server
|
||||
// track clients logged in to accounts
|
||||
accountToClients map[string][]*Client
|
||||
nickToAccount map[string]string
|
||||
}
|
||||
|
||||
func NewAccountManager(server *Server) *AccountManager {
|
||||
am := AccountManager{
|
||||
accountToClients: make(map[string][]*Client),
|
||||
nickToAccount: make(map[string]string),
|
||||
server: server,
|
||||
}
|
||||
|
||||
am.buildNickToAccountIndex()
|
||||
return &am
|
||||
}
|
||||
|
||||
func (am *AccountManager) buildNickToAccountIndex() {
|
||||
if am.server.AccountConfig().NickReservation == NickReservationDisabled {
|
||||
return
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
existsPrefix := fmt.Sprintf(keyAccountExists, "")
|
||||
|
||||
am.serialCacheUpdateMutex.Lock()
|
||||
defer am.serialCacheUpdateMutex.Unlock()
|
||||
|
||||
err := am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
err := tx.AscendGreaterOrEqual("", existsPrefix, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, existsPrefix) {
|
||||
return false
|
||||
}
|
||||
accountName := strings.TrimPrefix(key, existsPrefix)
|
||||
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil {
|
||||
result[accountName] = accountName
|
||||
}
|
||||
return true
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", fmt.Sprintf("couldn't read reserved nicks: %v", err))
|
||||
} else {
|
||||
am.Lock()
|
||||
am.nickToAccount = result
|
||||
am.Unlock()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) NickToAccount(cfnick string) string {
|
||||
am.RLock()
|
||||
defer am.RUnlock()
|
||||
return am.nickToAccount[cfnick]
|
||||
}
|
||||
|
||||
func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
if err != nil || account == "" || account == "*" {
|
||||
return errAccountCreation
|
||||
}
|
||||
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
||||
|
||||
var creds AccountCredentials
|
||||
// always set passphrase salt
|
||||
creds.PassphraseSalt, err = passwd.NewSalt()
|
||||
if err != nil {
|
||||
return errAccountCreation
|
||||
}
|
||||
// it's fine if this is empty, that just means no certificate is authorized
|
||||
creds.Certificate = certfp
|
||||
if passphrase != "" {
|
||||
creds.PassphraseHash, err = am.server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
|
||||
return errAccountCreation
|
||||
}
|
||||
}
|
||||
|
||||
credText, err := json.Marshal(creds)
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err))
|
||||
return errAccountCreation
|
||||
}
|
||||
credStr := string(credText)
|
||||
|
||||
registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
var setOptions *buntdb.SetOptions
|
||||
ttl := am.server.AccountConfig().Registration.VerifyTimeout
|
||||
if ttl != 0 {
|
||||
setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
|
||||
}
|
||||
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
_, err := am.loadRawAccount(tx, casefoldedAccount)
|
||||
if err != errAccountDoesNotExist {
|
||||
return errAccountAlreadyRegistered
|
||||
}
|
||||
|
||||
if certfp != "" {
|
||||
// make sure certfp doesn't already exist because that'd be silly
|
||||
_, err := tx.Get(certFPKey)
|
||||
if err != buntdb.ErrNotFound {
|
||||
return errCertfpAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
tx.Set(accountKey, "1", setOptions)
|
||||
tx.Set(accountNameKey, account, setOptions)
|
||||
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
|
||||
tx.Set(credentialsKey, credStr, setOptions)
|
||||
if certfp != "" {
|
||||
tx.Set(certFPKey, casefoldedAccount, setOptions)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) Verify(client *Client, account string, code string) error {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
if err != nil || account == "" || account == "*" {
|
||||
return errAccountVerificationFailed
|
||||
}
|
||||
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
|
||||
var raw rawClientAccount
|
||||
|
||||
func() {
|
||||
am.serialCacheUpdateMutex.Lock()
|
||||
defer am.serialCacheUpdateMutex.Unlock()
|
||||
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
raw, err = am.loadRawAccount(tx, casefoldedAccount)
|
||||
if err == errAccountDoesNotExist {
|
||||
return errAccountDoesNotExist
|
||||
} else if err != nil {
|
||||
return errAccountVerificationFailed
|
||||
} else if raw.Verified {
|
||||
return errAccountAlreadyVerified
|
||||
}
|
||||
|
||||
// TODO add code verification here
|
||||
// return errAccountVerificationFailed if it fails
|
||||
|
||||
// verify the account
|
||||
tx.Set(verifiedKey, "1", nil)
|
||||
// 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(credentialsKey, raw.Credentials, nil)
|
||||
|
||||
var creds AccountCredentials
|
||||
// XXX we shouldn't do (de)serialization inside the txn,
|
||||
// but this is like 2 usec on my system
|
||||
json.Unmarshal([]byte(raw.Credentials), &creds)
|
||||
if creds.Certificate != "" {
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
|
||||
tx.Set(certFPKey, casefoldedAccount, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
am.Lock()
|
||||
am.nickToAccount[casefoldedAccount] = casefoldedAccount
|
||||
am.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
am.Login(client, raw.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error {
|
||||
casefoldedAccount, err := CasefoldName(accountName)
|
||||
if err != nil {
|
||||
return errAccountDoesNotExist
|
||||
}
|
||||
|
||||
account, err := am.LoadAccount(casefoldedAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !account.Verified {
|
||||
return errAccountUnverified
|
||||
}
|
||||
|
||||
err = am.server.passwords.CompareHashAndPassword(
|
||||
account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase)
|
||||
if err != nil {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
|
||||
am.Login(client, account.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) LoadAccount(casefoldedAccount string) (result ClientAccount, err error) {
|
||||
var raw rawClientAccount
|
||||
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||
raw, err = am.loadRawAccount(tx, casefoldedAccount)
|
||||
if err == buntdb.ErrNotFound {
|
||||
err = errAccountDoesNotExist
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result.Name = raw.Name
|
||||
regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
|
||||
result.RegisteredAt = time.Unix(regTimeInt, 0)
|
||||
e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
|
||||
if e != nil {
|
||||
am.server.logger.Error("internal", fmt.Sprintf("could not unmarshal credentials: %v", e))
|
||||
err = errAccountDoesNotExist
|
||||
return
|
||||
}
|
||||
result.Verified = raw.Verified
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string) (result rawClientAccount, err error) {
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
|
||||
_, e := tx.Get(accountKey)
|
||||
if e == buntdb.ErrNotFound {
|
||||
err = errAccountDoesNotExist
|
||||
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
|
||||
}
|
||||
if _, e = tx.Get(verifiedKey); e == nil {
|
||||
result.Verified = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) Unregister(account string) error {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
if err != nil {
|
||||
return errAccountDoesNotExist
|
||||
}
|
||||
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
|
||||
var clients []*Client
|
||||
|
||||
func() {
|
||||
var credText string
|
||||
|
||||
am.serialCacheUpdateMutex.Lock()
|
||||
defer am.serialCacheUpdateMutex.Unlock()
|
||||
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Delete(accountKey)
|
||||
tx.Delete(accountNameKey)
|
||||
tx.Delete(verifiedKey)
|
||||
tx.Delete(registeredTimeKey)
|
||||
credText, err = tx.Get(credentialsKey)
|
||||
tx.Delete(credentialsKey)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
var creds AccountCredentials
|
||||
if err = json.Unmarshal([]byte(credText), &creds); err == nil && creds.Certificate != "" {
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
|
||||
tx.Delete(certFPKey)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
clients = am.accountToClients[casefoldedAccount]
|
||||
delete(am.accountToClients, casefoldedAccount)
|
||||
// TODO when registration of multiple nicks is fully implemented,
|
||||
// save the nicks that were deleted from the store and delete them here:
|
||||
delete(am.nickToAccount, casefoldedAccount)
|
||||
}()
|
||||
|
||||
for _, client := range clients {
|
||||
client.LogoutOfAccount()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
|
||||
if client.certfp == "" {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
|
||||
var account string
|
||||
var rawAccount rawClientAccount
|
||||
certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)
|
||||
|
||||
err := am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
var err error
|
||||
account, _ = tx.Get(certFPKey)
|
||||
if account == "" {
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
rawAccount, err = am.loadRawAccount(tx, account)
|
||||
if err != nil || !rawAccount.Verified {
|
||||
return errAccountUnverified
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ok, we found an account corresponding to their certificate
|
||||
|
||||
am.Login(client, rawAccount.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) Login(client *Client, account string) {
|
||||
client.LoginToAccount(account)
|
||||
|
||||
casefoldedAccount, _ := CasefoldName(account)
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
|
||||
}
|
||||
|
||||
func (am *AccountManager) Logout(client *Client) {
|
||||
casefoldedAccount := client.Account()
|
||||
if casefoldedAccount == "" || casefoldedAccount == "*" {
|
||||
return
|
||||
}
|
||||
|
||||
client.LogoutOfAccount()
|
||||
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
|
||||
if client.LoggedIntoAccount() {
|
||||
return
|
||||
}
|
||||
|
||||
clients := am.accountToClients[casefoldedAccount]
|
||||
if len(clients) <= 1 {
|
||||
delete(am.accountToClients, casefoldedAccount)
|
||||
return
|
||||
}
|
||||
remainingClients := make([]*Client, len(clients)-1)
|
||||
remainingPos := 0
|
||||
for currentPos := 0; currentPos < len(clients); currentPos++ {
|
||||
if clients[currentPos] != client {
|
||||
remainingClients[remainingPos] = clients[currentPos]
|
||||
remainingPos++
|
||||
}
|
||||
}
|
||||
am.accountToClients[casefoldedAccount] = remainingClients
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support.
|
||||
// This can be moved to some other data structure/place if we need to load/unload mechs later.
|
||||
@ -31,95 +451,62 @@ var (
|
||||
"PLAIN": authPlainHandler,
|
||||
"EXTERNAL": authExternalHandler,
|
||||
}
|
||||
|
||||
// NoAccount is a placeholder which means that the user is not logged into an account.
|
||||
NoAccount = ClientAccount{
|
||||
Name: "*", // * is used until actual account name is set
|
||||
}
|
||||
)
|
||||
|
||||
// AccountCredentials stores the various methods for verifying accounts.
|
||||
type AccountCredentials struct {
|
||||
PassphraseSalt []byte
|
||||
PassphraseHash []byte
|
||||
Certificate string // fingerprint
|
||||
}
|
||||
|
||||
// ClientAccount represents a user account.
|
||||
type ClientAccount struct {
|
||||
// Name of the account.
|
||||
Name string
|
||||
// RegisteredAt represents the time that the account was registered.
|
||||
RegisteredAt time.Time
|
||||
// Clients that are currently logged into this account (useful for notifications).
|
||||
Clients []*Client
|
||||
Credentials AccountCredentials
|
||||
Verified bool
|
||||
}
|
||||
|
||||
// loadAccountCredentials loads an account's credentials from the store.
|
||||
func loadAccountCredentials(tx *buntdb.Tx, accountKey string) (*AccountCredentials, error) {
|
||||
credText, err := tx.Get(fmt.Sprintf(keyAccountCredentials, accountKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var creds AccountCredentials
|
||||
err = json.Unmarshal([]byte(credText), &creds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &creds, nil
|
||||
}
|
||||
|
||||
// loadAccount loads an account from the store, note that the account must actually exist.
|
||||
func loadAccount(server *Server, tx *buntdb.Tx, accountKey string) *ClientAccount {
|
||||
name, _ := tx.Get(fmt.Sprintf(keyAccountName, accountKey))
|
||||
regTime, _ := tx.Get(fmt.Sprintf(keyAccountRegTime, accountKey))
|
||||
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
|
||||
accountInfo := ClientAccount{
|
||||
Name: name,
|
||||
RegisteredAt: time.Unix(regTimeInt, 0),
|
||||
Clients: []*Client{},
|
||||
}
|
||||
server.accounts[accountKey] = &accountInfo
|
||||
|
||||
return &accountInfo
|
||||
// convenience for passing around raw serialized account data
|
||||
type rawClientAccount struct {
|
||||
Name string
|
||||
RegisteredAt string
|
||||
Credentials string
|
||||
Verified bool
|
||||
}
|
||||
|
||||
// LoginToAccount logs the client into the given account.
|
||||
func (client *Client) LoginToAccount(account *ClientAccount) {
|
||||
if client.account == account {
|
||||
// already logged into this acct, no changing necessary
|
||||
func (client *Client) LoginToAccount(account string) {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
if err != nil {
|
||||
return
|
||||
} else if client.LoggedIntoAccount() {
|
||||
// logout of existing acct
|
||||
var newClientAccounts []*Client
|
||||
for _, c := range account.Clients {
|
||||
if c != client {
|
||||
newClientAccounts = append(newClientAccounts, c)
|
||||
}
|
||||
}
|
||||
account.Clients = newClientAccounts
|
||||
}
|
||||
|
||||
account.Clients = append(account.Clients, client)
|
||||
client.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.Name))
|
||||
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.
|
||||
func (client *Client) LogoutOfAccount() {
|
||||
account := client.account
|
||||
if account == nil {
|
||||
if client.Account() == "" {
|
||||
// already logged out
|
||||
return
|
||||
}
|
||||
|
||||
// logout of existing acct
|
||||
var newClientAccounts []*Client
|
||||
for _, c := range account.Clients {
|
||||
if c != client {
|
||||
newClientAccounts = append(newClientAccounts, c)
|
||||
}
|
||||
}
|
||||
account.Clients = newClientAccounts
|
||||
|
||||
client.account = nil
|
||||
client.SetAccountName("")
|
||||
client.nickTimer.Touch()
|
||||
|
||||
// dispatch account-notify
|
||||
for friend := range client.Friends(caps.AccountNotify) {
|
||||
@ -129,11 +516,11 @@ func (client *Client) LogoutOfAccount() {
|
||||
|
||||
// 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.account.Name, fmt.Sprintf("You are now logged in as %s", client.account.Name))
|
||||
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.account.Name)
|
||||
friend.Send(nil, client.nickMaskString, "ACCOUNT", client.AccountName())
|
||||
}
|
||||
}
|
||||
|
@ -383,13 +383,13 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) {
|
||||
for _, member := range channel.Members() {
|
||||
if member == client {
|
||||
if member.capabilities.Has(caps.ExtendedJoin) {
|
||||
rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
|
||||
rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname)
|
||||
} else {
|
||||
rb.Add(nil, client.nickMaskString, "JOIN", channel.name)
|
||||
}
|
||||
} else {
|
||||
if member.capabilities.Has(caps.ExtendedJoin) {
|
||||
member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
|
||||
member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname)
|
||||
} else {
|
||||
member.Send(nil, client.nickMaskString, "JOIN", channel.name)
|
||||
}
|
||||
@ -407,7 +407,9 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) {
|
||||
// give channel mode if necessary
|
||||
newChannel := firstJoin && !channel.IsRegistered()
|
||||
var givenMode *modes.Mode
|
||||
if client.AccountName() == channel.registeredFounder {
|
||||
account := client.Account()
|
||||
cffounder, _ := CasefoldName(channel.registeredFounder)
|
||||
if account != "" && account == cffounder {
|
||||
givenMode = &modes.ChannelFounder
|
||||
} else if newChannel {
|
||||
givenMode = &modes.ChannelOperator
|
||||
@ -419,7 +421,7 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) {
|
||||
}
|
||||
|
||||
if client.capabilities.Has(caps.ExtendedJoin) {
|
||||
rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
|
||||
rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname)
|
||||
} else {
|
||||
rb.Add(nil, client.nickMaskString, "JOIN", channel.name)
|
||||
}
|
||||
@ -526,7 +528,7 @@ func (channel *Channel) CanSpeak(client *Client) bool {
|
||||
if channel.flags[modes.Moderated] && !channel.ClientIsAtLeast(client, modes.Voice) {
|
||||
return false
|
||||
}
|
||||
if channel.flags[modes.RegisteredOnly] && client.account == &NoAccount {
|
||||
if channel.flags[modes.RegisteredOnly] && client.Account() == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@ -70,13 +70,13 @@ func (server *Server) chanservRegisterHandler(client *Client, channelName string
|
||||
return
|
||||
}
|
||||
|
||||
if client.account == &NoAccount {
|
||||
if client.Account() == "" {
|
||||
rb.ChanServNotice(client.t("You must be logged in to register a channel"))
|
||||
return
|
||||
}
|
||||
|
||||
// this provides the synchronization that allows exactly one registration of the channel:
|
||||
err = channelInfo.SetRegistered(client.AccountName())
|
||||
err = channelInfo.SetRegistered(client.Account())
|
||||
if err != nil {
|
||||
rb.ChanServNotice(err.Error())
|
||||
return
|
||||
|
@ -36,7 +36,8 @@ var (
|
||||
|
||||
// Client is an IRC client.
|
||||
type Client struct {
|
||||
account *ClientAccount
|
||||
account string
|
||||
accountName string
|
||||
atime time.Time
|
||||
authorized bool
|
||||
awayMessage string
|
||||
@ -62,6 +63,7 @@ type Client struct {
|
||||
nickCasefolded string
|
||||
nickMaskCasefolded string
|
||||
nickMaskString string // cache for nickmask string since it's used with lots of replies
|
||||
nickTimer *NickTimer
|
||||
operName string
|
||||
proxiedIP net.IP // actual remote IP if using the PROXY protocol
|
||||
quitMessage string
|
||||
@ -96,7 +98,6 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
|
||||
flags: make(map[modes.Mode]bool),
|
||||
server: server,
|
||||
socket: &socket,
|
||||
account: &NoAccount,
|
||||
nick: "*", // * is used until actual nick is given
|
||||
nickCasefolded: "*",
|
||||
nickMaskString: "*", // * is used until actual nick is given
|
||||
@ -217,6 +218,8 @@ func (client *Client) run() {
|
||||
client.idletimer = NewIdleTimer(client)
|
||||
client.idletimer.Start()
|
||||
|
||||
client.nickTimer = NewNickTimer(client)
|
||||
|
||||
// Set the hostname for this client
|
||||
// (may be overridden by a later PROXY command from stunnel)
|
||||
client.rawHostname = utils.AddrLookupHostname(client.socket.conn.RemoteAddr())
|
||||
@ -299,7 +302,6 @@ func (client *Client) Register() {
|
||||
client.TryResume()
|
||||
|
||||
// finish registration
|
||||
client.Touch()
|
||||
client.updateNickMask("")
|
||||
client.server.monitorManager.AlertAbout(client, true)
|
||||
}
|
||||
@ -338,8 +340,8 @@ func (client *Client) TryResume() {
|
||||
return
|
||||
}
|
||||
|
||||
oldAccountName := oldClient.AccountName()
|
||||
newAccountName := client.AccountName()
|
||||
oldAccountName := oldClient.Account()
|
||||
newAccountName := client.Account()
|
||||
|
||||
if oldAccountName == "" || newAccountName == "" || oldAccountName != newAccountName {
|
||||
client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, old and new clients must be logged into the same account"))
|
||||
@ -406,7 +408,7 @@ func (client *Client) TryResume() {
|
||||
}
|
||||
|
||||
if member.capabilities.Has(caps.ExtendedJoin) {
|
||||
member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
|
||||
member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname)
|
||||
} else {
|
||||
member.Send(nil, client.nickMaskString, "JOIN", channel.name)
|
||||
}
|
||||
@ -589,7 +591,7 @@ func (client *Client) AllNickmasks() []string {
|
||||
|
||||
// LoggedIntoAccount returns true if this client is logged into an account.
|
||||
func (client *Client) LoggedIntoAccount() bool {
|
||||
return client.account != nil && client.account != &NoAccount
|
||||
return client.Account() != ""
|
||||
}
|
||||
|
||||
// RplISupport outputs our ISUPPORT lines to the client. This is used on connection and in VERSION responses.
|
||||
@ -687,6 +689,8 @@ func (client *Client) destroy(beingResumed bool) {
|
||||
client.idletimer.Stop()
|
||||
}
|
||||
|
||||
client.server.accounts.Logout(client)
|
||||
|
||||
client.socket.Close()
|
||||
|
||||
// send quit messages to friends
|
||||
@ -723,11 +727,11 @@ func (client *Client) SendSplitMsgFromClient(msgid string, from *Client, tags *m
|
||||
// Adds account-tag to the line as well.
|
||||
func (client *Client) SendFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) error {
|
||||
// attach account-tag
|
||||
if client.capabilities.Has(caps.AccountTag) && from.account != &NoAccount {
|
||||
if client.capabilities.Has(caps.AccountTag) && client.LoggedIntoAccount() {
|
||||
if tags == nil {
|
||||
tags = ircmsg.MakeTags("account", from.account.Name)
|
||||
tags = ircmsg.MakeTags("account", from.AccountName())
|
||||
} else {
|
||||
(*tags)["account"] = ircmsg.MakeTagValue(from.account.Name)
|
||||
(*tags)["account"] = ircmsg.MakeTagValue(from.AccountName())
|
||||
}
|
||||
}
|
||||
// attach message-id
|
||||
@ -772,10 +776,8 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage) error {
|
||||
maxlenTags, maxlenRest := client.maxlens()
|
||||
line, err := message.LineMaxLen(maxlenTags, maxlenRest)
|
||||
if err != nil {
|
||||
// try not to fail quietly - especially useful when running tests, as a note to dig deeper
|
||||
// log.Println("Error assembling message:")
|
||||
// spew.Dump(message)
|
||||
// debug.PrintStack()
|
||||
logline := fmt.Sprintf("Error assembling message for sending: %v\n%s", err, debug.Stack())
|
||||
client.server.logger.Error("internal", logline)
|
||||
|
||||
message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
|
||||
line, _ := message.Line()
|
||||
|
@ -98,6 +98,12 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var reservedAccount string
|
||||
reservation := client.server.AccountConfig().NickReservation
|
||||
if reservation != NickReservationDisabled {
|
||||
reservedAccount = client.server.accounts.NickToAccount(newcfnick)
|
||||
}
|
||||
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
@ -107,6 +113,9 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
|
||||
if currentNewEntry != nil && currentNewEntry != client {
|
||||
return errNicknameInUse
|
||||
}
|
||||
if reservation == NickReservationStrict && reservedAccount != client.Account() {
|
||||
return errNicknameReserved
|
||||
}
|
||||
clients.byNick[newcfnick] = client
|
||||
client.updateNickMask(newNick)
|
||||
return nil
|
||||
|
@ -39,12 +39,7 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
|
||||
client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
|
||||
return false
|
||||
}
|
||||
if !cmd.leaveClientActive {
|
||||
client.Active()
|
||||
}
|
||||
if !cmd.leaveClientIdle {
|
||||
client.Touch()
|
||||
}
|
||||
|
||||
rb := NewResponseBuffer(client)
|
||||
rb.Label = GetLabel(msg)
|
||||
|
||||
@ -57,6 +52,14 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
|
||||
server.tryRegister(client)
|
||||
}
|
||||
|
||||
if !cmd.leaveClientIdle {
|
||||
client.Touch()
|
||||
}
|
||||
|
||||
if !cmd.leaveClientActive {
|
||||
client.Active()
|
||||
}
|
||||
|
||||
return exiting
|
||||
}
|
||||
|
||||
@ -67,7 +70,7 @@ func init() {
|
||||
Commands = map[string]Command{
|
||||
"ACC": {
|
||||
handler: accHandler,
|
||||
minParams: 3,
|
||||
minParams: 2,
|
||||
},
|
||||
"AMBIANCE": {
|
||||
handler: sceneHandler,
|
||||
@ -98,6 +101,7 @@ func init() {
|
||||
"DEBUG": {
|
||||
handler: debugHandler,
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
},
|
||||
"DLINE": {
|
||||
handler: dlineHandler,
|
||||
|
@ -8,6 +8,7 @@ package irc
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@ -57,10 +58,48 @@ func (conf *PassConfig) PasswordBytes() []byte {
|
||||
return bytes
|
||||
}
|
||||
|
||||
type NickReservation int
|
||||
|
||||
const (
|
||||
NickReservationDisabled NickReservation = iota
|
||||
NickReservationWithTimeout
|
||||
NickReservationStrict
|
||||
)
|
||||
|
||||
func (nr *NickReservation) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var orig, raw string
|
||||
var err error
|
||||
if err = unmarshal(&orig); err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, err = Casefold(orig); err != nil {
|
||||
return err
|
||||
}
|
||||
if raw == "disabled" || raw == "false" || raw == "" {
|
||||
*nr = NickReservationDisabled
|
||||
} else if raw == "timeout" {
|
||||
*nr = NickReservationWithTimeout
|
||||
} else if raw == "strict" {
|
||||
*nr = NickReservationStrict
|
||||
} else {
|
||||
return errors.New(fmt.Sprintf("invalid nick-reservation value: %s", orig))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AccountConfig struct {
|
||||
Registration AccountRegistrationConfig
|
||||
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
||||
NickReservation NickReservation `yaml:"nick-reservation"`
|
||||
NickReservationTimeout time.Duration `yaml:"nick-reservation-timeout"`
|
||||
}
|
||||
|
||||
// AccountRegistrationConfig controls account registration.
|
||||
type AccountRegistrationConfig struct {
|
||||
Enabled bool
|
||||
EnabledCallbacks []string `yaml:"enabled-callbacks"`
|
||||
EnabledCredentialTypes []string `yaml:"-"`
|
||||
VerifyTimeout time.Duration `yaml:"verify-timeout"`
|
||||
Callbacks struct {
|
||||
Mailto struct {
|
||||
Server string
|
||||
@ -180,10 +219,7 @@ type Config struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
Accounts struct {
|
||||
Registration AccountRegistrationConfig
|
||||
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
||||
}
|
||||
Accounts AccountConfig
|
||||
|
||||
Channels struct {
|
||||
DefaultModes *string `yaml:"default-modes"`
|
||||
@ -469,6 +505,15 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
config.Logging = newLogConfigs
|
||||
|
||||
// hardcode this for now
|
||||
config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"}
|
||||
for i, name := range config.Accounts.Registration.EnabledCallbacks {
|
||||
if name == "none" {
|
||||
// we store "none" as "*" internally
|
||||
config.Accounts.Registration.EnabledCallbacks[i] = "*"
|
||||
}
|
||||
}
|
||||
|
||||
config.Server.MaxSendQBytes, err = bytefmt.ToBytes(config.Server.MaxSendQString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())
|
||||
|
@ -9,7 +9,13 @@ 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")
|
||||
@ -17,6 +23,7 @@ var (
|
||||
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")
|
||||
|
@ -56,6 +56,12 @@ func (server *Server) ChannelRegistrationEnabled() bool {
|
||||
return server.channelRegistrationEnabled
|
||||
}
|
||||
|
||||
func (server *Server) AccountConfig() *AccountConfig {
|
||||
server.configurableStateMutex.RLock()
|
||||
defer server.configurableStateMutex.RUnlock()
|
||||
return server.accountConfig
|
||||
}
|
||||
|
||||
func (client *Client) Nick() string {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
@ -104,10 +110,30 @@ func (client *Client) Destroyed() bool {
|
||||
return client.isDestroyed
|
||||
}
|
||||
|
||||
func (client *Client) Account() string {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.account
|
||||
}
|
||||
|
||||
func (client *Client) AccountName() string {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.account.Name
|
||||
if client.accountName == "" {
|
||||
return "*"
|
||||
}
|
||||
return client.accountName
|
||||
}
|
||||
|
||||
func (client *Client) SetAccountName(account string) {
|
||||
var casefoldedAccount string
|
||||
if account != "" {
|
||||
casefoldedAccount, _ = CasefoldName(account)
|
||||
}
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
client.account = casefoldedAccount
|
||||
client.accountName = account
|
||||
}
|
||||
|
||||
func (client *Client) HasMode(mode modes.Mode) bool {
|
||||
|
272
irc/handlers.go
272
irc/handlers.go
@ -11,7 +11,6 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
@ -42,6 +41,8 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
return accRegisterHandler(server, client, msg, rb)
|
||||
} else if subcommand == "verify" {
|
||||
rb.Notice(client.t("VERIFY is not yet implemented"))
|
||||
} else if subcommand == "unregister" {
|
||||
return accUnregisterHandler(server, client, msg, rb)
|
||||
} else {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
|
||||
}
|
||||
@ -49,18 +50,45 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
return false
|
||||
}
|
||||
|
||||
// ACC UNREGISTER <accountname>
|
||||
func accUnregisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
// get and sanitise account name
|
||||
account := strings.TrimSpace(msg.Params[1])
|
||||
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 || msg.Params[1] == "*" {
|
||||
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, account, client.t("Account name is not valid"))
|
||||
return false
|
||||
}
|
||||
|
||||
if !(account == client.Account() || client.HasRoleCapabs("unregister")) {
|
||||
rb.Add(nil, server.name, ERR_NOPRIVS, client.Nick(), account, client.t("Insufficient oper privs"))
|
||||
return false
|
||||
}
|
||||
|
||||
err = server.accounts.Unregister(account)
|
||||
// TODO better responses all around here
|
||||
if err != nil {
|
||||
errorMsg := fmt.Sprintf("Unknown error while unregistering account %s", casefoldedAccount)
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), msg.Command, errorMsg)
|
||||
return false
|
||||
}
|
||||
rb.Notice(fmt.Sprintf("Successfully unregistered account %s", casefoldedAccount))
|
||||
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.accountRegistration.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
|
||||
}
|
||||
|
||||
// clients can't reg new accounts if they're already logged in
|
||||
if client.LoggedIntoAccount() {
|
||||
if server.accountRegistration.AllowMultiplePerConnection {
|
||||
client.LogoutOfAccount()
|
||||
if server.AccountConfig().Registration.AllowMultiplePerConnection {
|
||||
server.accounts.Logout(client)
|
||||
} else {
|
||||
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("You're already logged into an account"))
|
||||
return false
|
||||
@ -76,36 +104,11 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
||||
return false
|
||||
}
|
||||
|
||||
// check whether account exists
|
||||
// do it all in one write tx to prevent races
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
|
||||
_, err := tx.Get(accountKey)
|
||||
if err != buntdb.ErrNotFound {
|
||||
//TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be
|
||||
rb.Add(nil, server.name, ERR_ACCOUNT_ALREADY_EXISTS, client.nick, account, client.t("Account already exists"))
|
||||
return errAccountCreation
|
||||
}
|
||||
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
|
||||
tx.Set(accountKey, "1", nil)
|
||||
tx.Set(fmt.Sprintf(keyAccountName, casefoldedAccount), account, nil)
|
||||
tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil)
|
||||
return nil
|
||||
})
|
||||
|
||||
// account could not be created and relevant numerics have been dispatched, abort
|
||||
if err != nil {
|
||||
if err != errAccountCreation {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", client.t("Could not register"))
|
||||
log.Println("Could not save registration initial data:", err.Error())
|
||||
}
|
||||
if len(msg.Params) < 4 {
|
||||
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
|
||||
return false
|
||||
}
|
||||
|
||||
// account didn't already exist, continue with account creation and dispatching verification (if required)
|
||||
callback := strings.ToLower(msg.Params[2])
|
||||
var callbackNamespace, callbackValue string
|
||||
|
||||
@ -115,14 +118,14 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
||||
callbackValues := strings.SplitN(callback, ":", 2)
|
||||
callbackNamespace, callbackValue = callbackValues[0], callbackValues[1]
|
||||
} else {
|
||||
callbackNamespace = server.accountRegistration.EnabledCallbacks[0]
|
||||
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.accountRegistration.EnabledCallbacks {
|
||||
for _, name := range server.AccountConfig().Registration.EnabledCallbacks {
|
||||
if callbackNamespace == name {
|
||||
callbackValid = true
|
||||
}
|
||||
@ -130,7 +133,6 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
||||
|
||||
if !callbackValid {
|
||||
rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackNamespace, client.t("Callback namespace is not supported"))
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -140,116 +142,62 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
||||
if len(msg.Params) > 4 {
|
||||
credentialType = strings.ToLower(msg.Params[3])
|
||||
credentialValue = msg.Params[4]
|
||||
} else if len(msg.Params) == 4 {
|
||||
} else {
|
||||
// exactly 4 params
|
||||
credentialType = "passphrase" // default from the spec
|
||||
credentialValue = msg.Params[3]
|
||||
} else {
|
||||
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
// ensure the credential type is valid
|
||||
var credentialValid bool
|
||||
for _, name := range server.accountRegistration.EnabledCredentialTypes {
|
||||
for _, name := range server.AccountConfig().Registration.EnabledCredentialTypes {
|
||||
if credentialType == name {
|
||||
credentialValid = true
|
||||
}
|
||||
}
|
||||
if credentialType == "certfp" && client.certfp == "" {
|
||||
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
if !credentialValid {
|
||||
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
// store details
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
// certfp special lookup key
|
||||
var passphrase, certfp string
|
||||
if credentialType == "certfp" {
|
||||
assembledKeyCertToAccount := fmt.Sprintf(keyCertToAccount, client.certfp)
|
||||
|
||||
// make sure certfp doesn't already exist because that'd be silly
|
||||
_, err := tx.Get(assembledKeyCertToAccount)
|
||||
if err != buntdb.ErrNotFound {
|
||||
return errCertfpAlreadyExists
|
||||
}
|
||||
|
||||
tx.Set(assembledKeyCertToAccount, casefoldedAccount, nil)
|
||||
}
|
||||
|
||||
// make creds
|
||||
var creds AccountCredentials
|
||||
|
||||
// always set passphrase salt
|
||||
creds.PassphraseSalt, err = passwd.NewSalt()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not create passphrase salt: %s", err.Error())
|
||||
}
|
||||
|
||||
if credentialType == "certfp" {
|
||||
creds.Certificate = client.certfp
|
||||
certfp = client.certfp
|
||||
} else if credentialType == "passphrase" {
|
||||
creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, credentialValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not hash password: %s", err)
|
||||
passphrase = credentialValue
|
||||
}
|
||||
}
|
||||
credText, err := json.Marshal(creds)
|
||||
err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not marshal creds: %s", err)
|
||||
}
|
||||
tx.Set(fmt.Sprintf(keyAccountCredentials, account), string(credText), nil)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// details could not be stored and relevant numerics have been dispatched, abort
|
||||
if err != nil {
|
||||
errMsg := "Could not register"
|
||||
msg := "Unknown"
|
||||
code := ERR_UNKNOWNERROR
|
||||
if err == errCertfpAlreadyExists {
|
||||
errMsg = "An account already exists for your certificate fingerprint"
|
||||
msg = "An account already exists for your certificate fingerprint"
|
||||
} else if err == errAccountAlreadyRegistered {
|
||||
msg = "Account already exists"
|
||||
code = ERR_ACCOUNT_ALREADY_EXISTS
|
||||
}
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", errMsg)
|
||||
log.Println("Could not save registration creds:", err.Error())
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
|
||||
msg = err.Error()
|
||||
}
|
||||
rb.Add(nil, server.name, code, client.nick, "ACC", "REGISTER", client.t(msg))
|
||||
return false
|
||||
}
|
||||
|
||||
// automatically complete registration
|
||||
if callbackNamespace == "*" {
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(fmt.Sprintf(keyAccountVerified, casefoldedAccount), "1", nil)
|
||||
|
||||
// load acct info inside store tx
|
||||
account := ClientAccount{
|
||||
Name: strings.TrimSpace(msg.Params[1]),
|
||||
RegisteredAt: time.Now(),
|
||||
Clients: []*Client{client},
|
||||
}
|
||||
//TODO(dan): Consider creating ircd-wide account adding/removing/affecting lock for protecting access to these sorts of variables
|
||||
server.accounts[casefoldedAccount] = &account
|
||||
client.account = &account
|
||||
|
||||
rb.Add(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, account.Name, client.t("Account created"))
|
||||
rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account.Name, fmt.Sprintf(client.t("You are now logged in as %s"), account.Name))
|
||||
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]]"), account.Name, client.nickMaskString))
|
||||
return nil
|
||||
})
|
||||
err := server.accounts.Verify(client, casefoldedAccount, "")
|
||||
if err != nil {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", client.t("Could not register"))
|
||||
log.Println("Could not save verification confirmation (*):", err.Error())
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return false
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// dispatch callback
|
||||
@ -261,7 +209,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
|
||||
// AUTHENTICATE [<mechanism>|<data>|*]
|
||||
func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
// sasl abort
|
||||
if !server.accountAuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
|
||||
if !server.AccountConfig().AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
|
||||
rb.Add(nil, server.name, ERR_SASLABORTED, client.nick, client.t("SASL authentication aborted"))
|
||||
client.saslInProgress = false
|
||||
client.saslMechanism = ""
|
||||
@ -374,40 +322,11 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
|
||||
return false
|
||||
}
|
||||
|
||||
// load and check acct data all in one update to prevent races.
|
||||
// as noted elsewhere, change to proper locking for Account type later probably
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
// confirm account is verified
|
||||
_, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey))
|
||||
if err != nil {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
creds, err := loadAccountCredentials(tx, accountKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure creds are valid
|
||||
password := string(splitValue[2])
|
||||
if len(creds.PassphraseHash) < 1 || len(creds.PassphraseSalt) < 1 || len(password) < 1 {
|
||||
return errSaslFail
|
||||
}
|
||||
err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, password)
|
||||
|
||||
// succeeded, load account info if necessary
|
||||
account, exists := server.accounts[accountKey]
|
||||
if !exists {
|
||||
account = loadAccount(server, tx, accountKey)
|
||||
}
|
||||
|
||||
client.LoginToAccount(account)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
err = server.accounts.AuthenticateByPassphrase(client, accountKey, password)
|
||||
if err != nil {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed"))
|
||||
msg := authErrorToMessage(server, err)
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
|
||||
return false
|
||||
}
|
||||
|
||||
@ -415,6 +334,16 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
|
||||
return false
|
||||
}
|
||||
|
||||
func authErrorToMessage(server *Server, err error) (msg string) {
|
||||
if err == errAccountDoesNotExist || err == errAccountUnverified || err == errAccountInvalidCredentials {
|
||||
msg = err.Error()
|
||||
} else {
|
||||
server.logger.Error("internal", fmt.Sprintf("sasl authentication failure: %v", err))
|
||||
msg = "Unknown"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AUTHENTICATE EXTERNAL
|
||||
func authExternalHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool {
|
||||
if client.certfp == "" {
|
||||
@ -422,44 +351,10 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
|
||||
return false
|
||||
}
|
||||
|
||||
err := server.store.Update(func(tx *buntdb.Tx) error {
|
||||
// certfp lookup key
|
||||
accountKey, err := tx.Get(fmt.Sprintf(keyCertToAccount, client.certfp))
|
||||
err := server.accounts.AuthenticateByCertFP(client)
|
||||
if err != nil {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
// confirm account exists
|
||||
_, err = tx.Get(fmt.Sprintf(keyAccountExists, accountKey))
|
||||
if err != nil {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
// confirm account is verified
|
||||
_, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey))
|
||||
if err != nil {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
// confirm the certfp in that account's credentials
|
||||
creds, err := loadAccountCredentials(tx, accountKey)
|
||||
if err != nil || creds.Certificate != client.certfp {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
// succeeded, load account info if necessary
|
||||
account, exists := server.accounts[accountKey]
|
||||
if !exists {
|
||||
account = loadAccount(server, tx, accountKey)
|
||||
}
|
||||
|
||||
client.LoginToAccount(account)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed"))
|
||||
msg := authErrorToMessage(server, err)
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
|
||||
return false
|
||||
}
|
||||
|
||||
@ -582,11 +477,16 @@ func csHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respon
|
||||
|
||||
// DEBUG <subcmd>
|
||||
func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
if !client.flags[modes.Operator] {
|
||||
param, err := Casefold(msg.Params[0])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch msg.Params[0] {
|
||||
if !client.HasMode(modes.Operator) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch param {
|
||||
case "GCSTATS":
|
||||
stats := debug.GCStats{
|
||||
Pause: make([]time.Duration, 10),
|
||||
@ -2107,7 +2007,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
}
|
||||
|
||||
founder := channel.Founder()
|
||||
if founder != "" && founder != client.AccountName() {
|
||||
if founder != "" && founder != client.Account() {
|
||||
//TODO(dan): Change this to ERR_CANNOTRENAME
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "RENAME", oldName, client.t("Only channel founders can change registered channels"))
|
||||
return false
|
||||
@ -2130,11 +2030,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
} else {
|
||||
mcl.Send(nil, mcl.nickMaskString, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
|
||||
if mcl.capabilities.Has(caps.ExtendedJoin) {
|
||||
accountName := "*"
|
||||
if mcl.account != nil {
|
||||
accountName = mcl.account.Name
|
||||
}
|
||||
mcl.Send(nil, mcl.nickMaskString, "JOIN", newName, accountName, mcl.realname)
|
||||
mcl.Send(nil, mcl.nickMaskString, "JOIN", newName, mcl.AccountName(), mcl.realname)
|
||||
} else {
|
||||
mcl.Send(nil, mcl.nickMaskString, "JOIN", newName)
|
||||
}
|
||||
|
@ -165,3 +165,80 @@ func (it *IdleTimer) quitMessage(state TimerState) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// NickTimer manages timing out of clients who are squatting reserved nicks
|
||||
type NickTimer struct {
|
||||
sync.Mutex // tier 1
|
||||
|
||||
// immutable after construction
|
||||
timeout time.Duration
|
||||
client *Client
|
||||
|
||||
// mutable
|
||||
nick string
|
||||
accountForNick string
|
||||
account string
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
// NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled)
|
||||
func NewNickTimer(client *Client) *NickTimer {
|
||||
config := client.server.AccountConfig()
|
||||
if config.NickReservation != NickReservationWithTimeout {
|
||||
return nil
|
||||
}
|
||||
nt := NickTimer{
|
||||
client: client,
|
||||
timeout: config.NickReservationTimeout,
|
||||
}
|
||||
return &nt
|
||||
}
|
||||
|
||||
// Touch records a nick change and updates the timer as necessary
|
||||
func (nt *NickTimer) Touch() {
|
||||
if nt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
nick := nt.client.NickCasefolded()
|
||||
account := nt.client.Account()
|
||||
accountForNick := nt.client.server.accounts.NickToAccount(nick)
|
||||
|
||||
var shouldWarn bool
|
||||
|
||||
func() {
|
||||
nt.Lock()
|
||||
defer nt.Unlock()
|
||||
// the timer will not reset as long as the squatter is targeting the same account
|
||||
accountChanged := accountForNick != nt.accountForNick
|
||||
// change state
|
||||
nt.nick = nick
|
||||
nt.account = account
|
||||
nt.accountForNick = accountForNick
|
||||
delinquent := accountForNick != "" && accountForNick != account
|
||||
|
||||
if nt.timer != nil && (!delinquent || accountChanged) {
|
||||
nt.timer.Stop()
|
||||
nt.timer = nil
|
||||
}
|
||||
if delinquent && accountChanged {
|
||||
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
|
||||
shouldWarn = true
|
||||
}
|
||||
}()
|
||||
|
||||
if shouldWarn {
|
||||
nt.sendWarning()
|
||||
}
|
||||
}
|
||||
|
||||
func (nt *NickTimer) sendWarning() {
|
||||
baseNotice := "Nickname is reserved; you must change it or authenticate to NickServ within %v"
|
||||
nt.client.Notice(fmt.Sprintf(nt.client.t(baseNotice), nt.timeout))
|
||||
}
|
||||
|
||||
func (nt *NickTimer) processTimeout() {
|
||||
baseMsg := "Nick is reserved and authentication timeout expired: %v"
|
||||
nt.client.Quit(fmt.Sprintf(nt.client.t(baseMsg), nt.timeout))
|
||||
nt.client.destroy(false)
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ var (
|
||||
"=scene=": true, // used for rp commands
|
||||
"chanserv": true,
|
||||
"nickserv": true,
|
||||
"hostserv": true,
|
||||
}
|
||||
)
|
||||
|
||||
@ -45,11 +46,16 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
|
||||
if err == errNicknameInUse {
|
||||
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, client.nick, nickname, client.t("Nickname is already in use"))
|
||||
return false
|
||||
} else if err == errNicknameReserved {
|
||||
client.Send(nil, server.name, ERR_NICKNAMEINUSE, client.nick, nickname, client.t("Nickname is reserved by a different account"))
|
||||
return false
|
||||
} else if err != nil {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
client.nickTimer.Touch()
|
||||
|
||||
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
|
||||
if hadNick {
|
||||
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), origNick, nickname))
|
||||
|
185
irc/nickserv.go
185
irc/nickserv.go
@ -4,16 +4,11 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const nickservHelp = `NickServ lets you register and log into a user account.
|
||||
@ -80,14 +75,14 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
|
||||
return
|
||||
}
|
||||
|
||||
if !server.accountRegistration.Enabled {
|
||||
if !server.AccountConfig().Registration.Enabled {
|
||||
rb.Notice(client.t("Account registration has been disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
if client.LoggedIntoAccount() {
|
||||
if server.accountRegistration.AllowMultiplePerConnection {
|
||||
client.LogoutOfAccount()
|
||||
if server.AccountConfig().Registration.AllowMultiplePerConnection {
|
||||
server.accounts.Logout(client)
|
||||
} else {
|
||||
rb.Notice(client.t("You're already logged into an account"))
|
||||
return
|
||||
@ -103,26 +98,6 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
|
||||
return
|
||||
}
|
||||
|
||||
// check whether account exists
|
||||
// do it all in one write tx to prevent races
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
||||
|
||||
_, err := tx.Get(accountKey)
|
||||
if err != buntdb.ErrNotFound {
|
||||
//TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be
|
||||
rb.Notice(client.t("Account already exists"))
|
||||
return errAccountCreation
|
||||
}
|
||||
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
||||
|
||||
tx.Set(accountKey, "1", nil)
|
||||
tx.Set(fmt.Sprintf(keyAccountName, casefoldedAccount), account, nil)
|
||||
tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil)
|
||||
return nil
|
||||
})
|
||||
|
||||
// account could not be created and relevant numerics have been dispatched, abort
|
||||
if err != nil {
|
||||
if err != errAccountCreation {
|
||||
@ -131,87 +106,32 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
|
||||
return
|
||||
}
|
||||
|
||||
// store details
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
// certfp special lookup key
|
||||
if passphrase == "" {
|
||||
assembledKeyCertToAccount := fmt.Sprintf(keyCertToAccount, client.certfp)
|
||||
|
||||
// make sure certfp doesn't already exist because that'd be silly
|
||||
_, err := tx.Get(assembledKeyCertToAccount)
|
||||
if err != buntdb.ErrNotFound {
|
||||
return errCertfpAlreadyExists
|
||||
err = server.accounts.Register(client, account, "", "", passphrase, client.certfp)
|
||||
if err == nil {
|
||||
err = server.accounts.Verify(client, casefoldedAccount, "")
|
||||
}
|
||||
|
||||
tx.Set(assembledKeyCertToAccount, casefoldedAccount, nil)
|
||||
}
|
||||
|
||||
// make creds
|
||||
var creds AccountCredentials
|
||||
|
||||
// always set passphrase salt
|
||||
creds.PassphraseSalt, err = passwd.NewSalt()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not create passphrase salt: %s", err.Error())
|
||||
}
|
||||
|
||||
if passphrase == "" {
|
||||
creds.Certificate = client.certfp
|
||||
} else {
|
||||
creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not hash password: %s", err)
|
||||
}
|
||||
}
|
||||
credText, err := json.Marshal(creds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not marshal creds: %s", err)
|
||||
}
|
||||
tx.Set(fmt.Sprintf(keyAccountCredentials, account), string(credText), nil)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// details could not be stored and relevant numerics have been dispatched, abort
|
||||
if err != nil {
|
||||
errMsg := "Could not register"
|
||||
if err == errCertfpAlreadyExists {
|
||||
errMsg = "An account already exists for your certificate fingerprint"
|
||||
} else if err == errAccountAlreadyRegistered {
|
||||
errMsg = "Account already exists"
|
||||
}
|
||||
rb.Notice(errMsg)
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
rb.Notice(client.t(errMsg))
|
||||
return
|
||||
}
|
||||
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(fmt.Sprintf(keyAccountVerified, casefoldedAccount), "1", nil)
|
||||
|
||||
// load acct info inside store tx
|
||||
account := ClientAccount{
|
||||
Name: username,
|
||||
RegisteredAt: time.Now(),
|
||||
Clients: []*Client{client},
|
||||
}
|
||||
//TODO(dan): Consider creating ircd-wide account adding/removing/affecting lock for protecting access to these sorts of variables
|
||||
server.accounts[casefoldedAccount] = &account
|
||||
client.account = &account
|
||||
|
||||
rb.Notice(client.t("Account created"))
|
||||
rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account.Name, fmt.Sprintf(client.t("You are now logged in as %s"), account.Name))
|
||||
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]]"), account.Name, client.nickMaskString))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
rb.Notice(client.t("Account registration failed"))
|
||||
removeFailedAccRegisterData(server.store, casefoldedAccount)
|
||||
return
|
||||
}
|
||||
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) {
|
||||
// fail out if we need to
|
||||
if !server.accountAuthenticationEnabled {
|
||||
if !server.AccountConfig().AuthenticationEnabled {
|
||||
rb.Notice(client.t("Login has been disabled"))
|
||||
return
|
||||
}
|
||||
@ -219,45 +139,13 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra
|
||||
// try passphrase
|
||||
if username != "" && passphrase != "" {
|
||||
// keep it the same as in the ACC CREATE stage
|
||||
accountKey, err := CasefoldName(username)
|
||||
accountName, err := CasefoldName(username)
|
||||
if err != nil {
|
||||
rb.Notice(client.t("Could not login with your username/password"))
|
||||
return
|
||||
}
|
||||
|
||||
// load and check acct data all in one update to prevent races.
|
||||
// as noted elsewhere, change to proper locking for Account type later probably
|
||||
var accountName string
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
// confirm account is verified
|
||||
_, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey))
|
||||
if err != nil {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
creds, err := loadAccountCredentials(tx, accountKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure creds are valid
|
||||
if len(creds.PassphraseHash) < 1 || len(creds.PassphraseSalt) < 1 || len(passphrase) < 1 {
|
||||
return errSaslFail
|
||||
}
|
||||
err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, passphrase)
|
||||
|
||||
// succeeded, load account info if necessary
|
||||
account, exists := server.accounts[accountKey]
|
||||
if !exists {
|
||||
account = loadAccount(server, tx, accountKey)
|
||||
}
|
||||
|
||||
client.LoginToAccount(account)
|
||||
accountName = account.Name
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
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
|
||||
@ -265,48 +153,11 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra
|
||||
}
|
||||
|
||||
// try certfp
|
||||
certfp := client.certfp
|
||||
if certfp != "" {
|
||||
var accountName string
|
||||
err := server.store.Update(func(tx *buntdb.Tx) error {
|
||||
// certfp lookup key
|
||||
accountKey, err := tx.Get(fmt.Sprintf(keyCertToAccount, certfp))
|
||||
if err != nil {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
// confirm account exists
|
||||
_, err = tx.Get(fmt.Sprintf(keyAccountExists, accountKey))
|
||||
if err != nil {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
// confirm account is verified
|
||||
_, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey))
|
||||
if err != nil {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
// confirm the certfp in that account's credentials
|
||||
creds, err := loadAccountCredentials(tx, accountKey)
|
||||
if err != nil || creds.Certificate != client.certfp {
|
||||
return errSaslFail
|
||||
}
|
||||
|
||||
// succeeded, load account info if necessary
|
||||
account, exists := server.accounts[accountKey]
|
||||
if !exists {
|
||||
account = loadAccount(server, tx, accountKey)
|
||||
}
|
||||
|
||||
client.LoginToAccount(account)
|
||||
accountName = account.Name
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if client.certfp != "" {
|
||||
err := server.accounts.AuthenticateByCertFP(client)
|
||||
if err == nil {
|
||||
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName))
|
||||
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName()))
|
||||
// TODO more notices?
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -43,11 +43,11 @@ func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, c
|
||||
// AddFromClient adds a new message from a specific client to our queue.
|
||||
func (rb *ResponseBuffer) AddFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) {
|
||||
// attach account-tag
|
||||
if rb.target.capabilities.Has(caps.AccountTag) && from.account != &NoAccount {
|
||||
if rb.target.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() {
|
||||
if tags == nil {
|
||||
tags = ircmsg.MakeTags("account", from.account.Name)
|
||||
tags = ircmsg.MakeTags("account", from.AccountName())
|
||||
} else {
|
||||
(*tags)["account"] = ircmsg.MakeTagValue(from.account.Name)
|
||||
(*tags)["account"] = ircmsg.MakeTagValue(from.AccountName())
|
||||
}
|
||||
}
|
||||
// attach message-id
|
||||
|
@ -87,9 +87,8 @@ type ListenerWrapper struct {
|
||||
|
||||
// Server is the main Oragono server.
|
||||
type Server struct {
|
||||
accountAuthenticationEnabled bool
|
||||
accountRegistration *AccountRegistration
|
||||
accounts map[string]*ClientAccount
|
||||
accountConfig *AccountConfig
|
||||
accounts *AccountManager
|
||||
batches *BatchManager
|
||||
channelRegistrationEnabled bool
|
||||
channels *ChannelManager
|
||||
@ -121,7 +120,7 @@ type Server struct {
|
||||
password []byte
|
||||
passwords *passwd.SaltedManager
|
||||
recoverFromErrors bool
|
||||
rehashMutex sync.Mutex // tier 3
|
||||
rehashMutex sync.Mutex // tier 4
|
||||
rehashSignal chan os.Signal
|
||||
proxyAllowedFrom []string
|
||||
signals chan os.Signal
|
||||
@ -150,7 +149,6 @@ type clientConn struct {
|
||||
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
||||
// initialize data structures
|
||||
server := &Server{
|
||||
accounts: make(map[string]*ClientAccount),
|
||||
batches: NewBatchManager(),
|
||||
channels: NewChannelManager(),
|
||||
clients: NewClientManager(),
|
||||
@ -214,10 +212,10 @@ func (server *Server) setISupport() {
|
||||
isupport.Add("UTF8MAPPING", casemappingName)
|
||||
|
||||
// account registration
|
||||
if server.accountRegistration.Enabled {
|
||||
if server.accountConfig.Registration.Enabled {
|
||||
// 'none' isn't shown in the REGCALLBACKS vars
|
||||
var enabledCallbacks []string
|
||||
for _, name := range server.accountRegistration.EnabledCallbacks {
|
||||
for _, name := range server.accountConfig.Registration.EnabledCallbacks {
|
||||
if name != "*" {
|
||||
enabledCallbacks = append(enabledCallbacks, name)
|
||||
}
|
||||
@ -348,15 +346,8 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config) *Listen
|
||||
// make listener
|
||||
var listener net.Listener
|
||||
var err error
|
||||
optionalUnixPrefix := "unix:"
|
||||
optionalPrefixLen := len(optionalUnixPrefix)
|
||||
if len(addr) >= optionalPrefixLen && strings.ToLower(addr[0:optionalPrefixLen]) == optionalUnixPrefix {
|
||||
addr = addr[optionalPrefixLen:]
|
||||
if len(addr) == 0 || addr[0] != '/' {
|
||||
log.Fatal("Bad unix socket address", addr)
|
||||
}
|
||||
}
|
||||
if len(addr) > 0 && addr[0] == '/' {
|
||||
addr = strings.TrimPrefix(addr, "unix:")
|
||||
if strings.HasPrefix(addr, "/") {
|
||||
// https://stackoverflow.com/a/34881585
|
||||
os.Remove(addr)
|
||||
listener, err = net.Listen("unix", addr)
|
||||
@ -478,7 +469,7 @@ func (server *Server) tryRegister(c *Client) {
|
||||
}
|
||||
|
||||
if c.capabilities.Has(caps.ExtendedJoin) {
|
||||
c.Send(nil, c.nickMaskString, "JOIN", channel.name, c.account.Name, c.realname)
|
||||
c.Send(nil, c.nickMaskString, "JOIN", channel.name, c.AccountName(), c.realname)
|
||||
} else {
|
||||
c.Send(nil, c.nickMaskString, "JOIN", channel.name)
|
||||
}
|
||||
@ -630,9 +621,8 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
||||
if target.flags[modes.TLS] {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, client.t("is using a secure connection"))
|
||||
}
|
||||
accountName := target.AccountName()
|
||||
if accountName != "" {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, accountName, client.t("is logged in as"))
|
||||
if target.LoggedIntoAccount() {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, client.AccountName(), client.t("is logged in as"))
|
||||
}
|
||||
if target.flags[modes.Bot] {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.networkName)))
|
||||
@ -803,18 +793,28 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
||||
server.languages = lm
|
||||
|
||||
// SASL
|
||||
if config.Accounts.AuthenticationEnabled && !server.accountAuthenticationEnabled {
|
||||
oldAccountConfig := server.AccountConfig()
|
||||
authPreviouslyEnabled := oldAccountConfig != nil && !oldAccountConfig.AuthenticationEnabled
|
||||
if config.Accounts.AuthenticationEnabled && !authPreviouslyEnabled {
|
||||
// enabling SASL
|
||||
SupportedCapabilities.Enable(caps.SASL)
|
||||
CapValues.Set(caps.SASL, "PLAIN,EXTERNAL")
|
||||
addedCaps.Add(caps.SASL)
|
||||
}
|
||||
if !config.Accounts.AuthenticationEnabled && server.accountAuthenticationEnabled {
|
||||
} else if !config.Accounts.AuthenticationEnabled && authPreviouslyEnabled {
|
||||
// disabling SASL
|
||||
SupportedCapabilities.Disable(caps.SASL)
|
||||
removedCaps.Add(caps.SASL)
|
||||
}
|
||||
server.accountAuthenticationEnabled = config.Accounts.AuthenticationEnabled
|
||||
|
||||
server.configurableStateMutex.Lock()
|
||||
server.accountConfig = &config.Accounts
|
||||
server.configurableStateMutex.Unlock()
|
||||
|
||||
nickReservationPreviouslyDisabled := oldAccountConfig != nil && oldAccountConfig.NickReservation == NickReservationDisabled
|
||||
nickReservationNowEnabled := config.Accounts.NickReservation != NickReservationDisabled
|
||||
if nickReservationPreviouslyDisabled && nickReservationNowEnabled {
|
||||
server.accounts.buildNickToAccountIndex()
|
||||
}
|
||||
|
||||
// STS
|
||||
stsValue := config.Server.STS.Value()
|
||||
@ -902,8 +902,6 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
||||
server.checkIdent = config.Server.CheckIdent
|
||||
|
||||
// registration
|
||||
accountReg := NewAccountRegistration(config.Accounts.Registration)
|
||||
server.accountRegistration = &accountReg
|
||||
server.channelRegistrationEnabled = config.Channels.Registration.Enabled
|
||||
|
||||
server.defaultChannelModes = ParseDefaultChannelModes(config)
|
||||
@ -1043,6 +1041,8 @@ func (server *Server) loadDatastore(datastorePath string) error {
|
||||
|
||||
server.channelRegistry = NewChannelRegistry(server)
|
||||
|
||||
server.accounts = NewAccountManager(server)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -159,6 +159,14 @@ accounts:
|
||||
# is account authentication enabled?
|
||||
authentication-enabled: true
|
||||
|
||||
# will the server enforce that only the account holder can use the account name as a nick?
|
||||
# options:
|
||||
# `disabled`: no enforcement
|
||||
# `timeout` (auth to nickserv within some period of time or you're disconnected)
|
||||
# `strict`: must authenticate up front with SASL
|
||||
nick-reservation: disabled
|
||||
nick-reservation-timeout: 30s
|
||||
|
||||
# channel options
|
||||
channels:
|
||||
# modes that are set when new channels are created
|
||||
@ -210,6 +218,7 @@ oper-classes:
|
||||
capabilities:
|
||||
- "oper:rehash"
|
||||
- "oper:die"
|
||||
- "unregister"
|
||||
- "samode"
|
||||
|
||||
# ircd operators
|
||||
|
Loading…
Reference in New Issue
Block a user