3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-21 19:39:43 +01:00

refactor account registration, add nick enforcement

This commit is contained in:
Shivaram Lingamneni 2018-02-11 05:30:40 -05:00
parent fcd0a75469
commit ad73d68807
18 changed files with 865 additions and 602 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ package irc
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
@ -57,11 +58,49 @@ 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"`
Callbacks struct {
Enabled bool
EnabledCallbacks []string `yaml:"enabled-callbacks"`
EnabledCredentialTypes []string `yaml:"-"`
VerifyTimeout time.Duration `yaml:"verify-timeout"`
Callbacks struct {
Mailto struct {
Server string
Port int
@ -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())

View File

@ -9,18 +9,25 @@ import "errors"
// Runtime Errors
var (
errAccountCreation = errors.New("Account could not be created")
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")
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")
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")
)
// Socket Errors

View File

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

View File

@ -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
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
} 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)
}
}
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
var passphrase, certfp string
if credentialType == "certfp" {
certfp = client.certfp
} else if credentialType == "passphrase" {
passphrase = credentialValue
}
err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
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
})
password := string(splitValue[2])
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))
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
})
err := server.accounts.AuthenticateByCertFP(client)
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)
}

View File

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

View File

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

View File

@ -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
}
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
})
err = server.accounts.Register(client, account, "", "", passphrase, client.certfp)
if err == nil {
err = server.accounts.Verify(client, casefoldedAccount, "")
}
// 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_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
}
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) {
// 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
}
}

View File

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

View File

@ -87,49 +87,48 @@ type ListenerWrapper struct {
// Server is the main Oragono server.
type Server struct {
accountAuthenticationEnabled bool
accountRegistration *AccountRegistration
accounts map[string]*ClientAccount
batches *BatchManager
channelRegistrationEnabled bool
channels *ChannelManager
channelRegistry *ChannelRegistry
checkIdent bool
clients *ClientManager
configFilename string
configurableStateMutex sync.RWMutex // tier 1; generic protection for server state modified by rehash()
connectionLimiter *connection_limits.Limiter
connectionThrottler *connection_limits.Throttler
ctime time.Time
defaultChannelModes modes.Modes
dlines *DLineManager
loggingRawIO bool
isupport *isupport.List
klines *KLineManager
languages *languages.Manager
limits Limits
listeners map[string]*ListenerWrapper
logger *logger.Manager
MaxSendQBytes uint64
monitorManager *MonitorManager
motdLines []string
name string
nameCasefolded string
networkName string
operators map[string]Oper
operclasses map[string]OperClass
password []byte
passwords *passwd.SaltedManager
recoverFromErrors bool
rehashMutex sync.Mutex // tier 3
rehashSignal chan os.Signal
proxyAllowedFrom []string
signals chan os.Signal
snomasks *SnoManager
store *buntdb.DB
stsEnabled bool
webirc []webircConfig
whoWas *WhoWasList
accountConfig *AccountConfig
accounts *AccountManager
batches *BatchManager
channelRegistrationEnabled bool
channels *ChannelManager
channelRegistry *ChannelRegistry
checkIdent bool
clients *ClientManager
configFilename string
configurableStateMutex sync.RWMutex // tier 1; generic protection for server state modified by rehash()
connectionLimiter *connection_limits.Limiter
connectionThrottler *connection_limits.Throttler
ctime time.Time
defaultChannelModes modes.Modes
dlines *DLineManager
loggingRawIO bool
isupport *isupport.List
klines *KLineManager
languages *languages.Manager
limits Limits
listeners map[string]*ListenerWrapper
logger *logger.Manager
MaxSendQBytes uint64
monitorManager *MonitorManager
motdLines []string
name string
nameCasefolded string
networkName string
operators map[string]Oper
operclasses map[string]OperClass
password []byte
passwords *passwd.SaltedManager
recoverFromErrors bool
rehashMutex sync.Mutex // tier 4
rehashSignal chan os.Signal
proxyAllowedFrom []string
signals chan os.Signal
snomasks *SnoManager
store *buntdb.DB
stsEnabled bool
webirc []webircConfig
whoWas *WhoWasList
}
var (
@ -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
}

View File

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