3
0
mirror of https://github.com/ergochat/ergo.git synced 2025-01-09 19:52:57 +01:00

Merge pull request #324 from slingamn/misc_again.5

some more changes
This commit is contained in:
Daniel Oaks 2019-01-03 09:16:44 +10:00 committed by GitHub
commit 0e22f8d6a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 602 additions and 209 deletions

View File

@ -20,6 +20,7 @@ test:
python3 ./gencapdefs.py | diff - ${capdef_file} python3 ./gencapdefs.py | diff - ${capdef_file}
cd irc && go test . && go vet . cd irc && go test . && go vet .
cd irc/caps && go test . && go vet . cd irc/caps && go test . && go vet .
cd irc/connection_limits && go test . && go vet .
cd irc/history && go test . && go vet . cd irc/history && go test . && go vet .
cd irc/isupport && go test . && go vet . cd irc/isupport && go test . && go vet .
cd irc/modes && go test . && go vet . cd irc/modes && go test . && go vet .

View File

@ -30,6 +30,7 @@ const (
keyAccountRegTime = "account.registered.time %s" keyAccountRegTime = "account.registered.time %s"
keyAccountCredentials = "account.credentials %s" keyAccountCredentials = "account.credentials %s"
keyAccountAdditionalNicks = "account.additionalnicks %s" keyAccountAdditionalNicks = "account.additionalnicks %s"
keyAccountEnforcement = "account.customenforcement %s"
keyAccountVHost = "account.vhost %s" keyAccountVHost = "account.vhost %s"
keyCertToAccount = "account.creds.certfp %s" keyCertToAccount = "account.creds.certfp %s"
@ -53,12 +54,14 @@ type AccountManager struct {
// track clients logged in to accounts // track clients logged in to accounts
accountToClients map[string][]*Client accountToClients map[string][]*Client
nickToAccount map[string]string nickToAccount map[string]string
accountToMethod map[string]NickReservationMethod
} }
func NewAccountManager(server *Server) *AccountManager { func NewAccountManager(server *Server) *AccountManager {
am := AccountManager{ am := AccountManager{
accountToClients: make(map[string][]*Client), accountToClients: make(map[string][]*Client),
nickToAccount: make(map[string]string), nickToAccount: make(map[string]string),
accountToMethod: make(map[string]NickReservationMethod),
server: server, server: server,
} }
@ -72,7 +75,8 @@ func (am *AccountManager) buildNickToAccountIndex() {
return return
} }
result := make(map[string]string) nickToAccount := make(map[string]string)
accountToMethod := make(map[string]NickReservationMethod)
existsPrefix := fmt.Sprintf(keyAccountExists, "") existsPrefix := fmt.Sprintf(keyAccountExists, "")
am.serialCacheUpdateMutex.Lock() am.serialCacheUpdateMutex.Lock()
@ -83,14 +87,22 @@ func (am *AccountManager) buildNickToAccountIndex() {
if !strings.HasPrefix(key, existsPrefix) { if !strings.HasPrefix(key, existsPrefix) {
return false return false
} }
accountName := strings.TrimPrefix(key, existsPrefix)
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil { account := strings.TrimPrefix(key, existsPrefix)
result[accountName] = accountName if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil {
nickToAccount[account] = account
} }
if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, accountName)); err == nil { if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, account)); err == nil {
additionalNicks := unmarshalReservedNicks(rawNicks) additionalNicks := unmarshalReservedNicks(rawNicks)
for _, nick := range additionalNicks { for _, nick := range additionalNicks {
result[nick] = accountName nickToAccount[nick] = account
}
}
if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil {
method, err := nickReservationFromString(methodStr)
if err == nil {
accountToMethod[account] = method
} }
} }
return true return true
@ -99,10 +111,11 @@ func (am *AccountManager) buildNickToAccountIndex() {
}) })
if err != nil { if err != nil {
am.server.logger.Error("internal", fmt.Sprintf("couldn't read reserved nicks: %v", err)) am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error())
} else { } else {
am.Lock() am.Lock()
am.nickToAccount = result am.nickToAccount = nickToAccount
am.accountToMethod = accountToMethod
am.Unlock() am.Unlock()
} }
} }
@ -156,6 +169,84 @@ func (am *AccountManager) NickToAccount(nick string) string {
return am.nickToAccount[cfnick] return am.nickToAccount[cfnick]
} }
// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
// used to enforce ownership.
func (am *AccountManager) EnforcementStatus(nick string) (account string, method NickReservationMethod) {
cfnick, err := CasefoldName(nick)
if err != nil {
return
}
config := am.server.Config()
if !config.Accounts.NickReservation.Enabled {
method = NickReservationNone
return
}
am.RLock()
defer am.RUnlock()
account = am.nickToAccount[cfnick]
method = am.accountToMethod[account]
// if they don't have a custom setting, or customization is disabled, use the default
if method == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
method = config.Accounts.NickReservation.Method
}
if method == NickReservationOptional {
// enforcement was explicitly enabled neither in the config or by the user
method = NickReservationNone
}
return
}
// Looks up the enforcement method stored in the database for an account
// (typically you want EnforcementStatus instead, which respects the config)
func (am *AccountManager) getStoredEnforcementStatus(account string) string {
am.RLock()
defer am.RUnlock()
return nickReservationToString(am.accountToMethod[account])
}
// Sets a custom enforcement method for an account and stores it in the database.
func (am *AccountManager) SetEnforcementStatus(account string, method NickReservationMethod) (err error) {
config := am.server.Config()
if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
return errFeatureDisabled
}
var serialized string
if method == NickReservationOptional {
serialized = "" // normally this is "default", but we're going to delete the key
} else {
serialized = nickReservationToString(method)
}
key := fmt.Sprintf(keyAccountEnforcement, account)
am.Lock()
defer am.Unlock()
currentMethod := am.accountToMethod[account]
if method != currentMethod {
if method == NickReservationOptional {
delete(am.accountToMethod, account)
} else {
am.accountToMethod[account] = method
}
return am.server.store.Update(func(tx *buntdb.Tx) (err error) {
if serialized != "" {
_, _, err = tx.Set(key, nickReservationToString(method), nil)
} else {
_, err = tx.Delete(key)
}
return
})
}
return nil
}
func (am *AccountManager) AccountToClients(account string) (result []*Client) { func (am *AccountManager) AccountToClients(account string) (result []*Client) {
cfaccount, err := CasefoldName(account) cfaccount, err := CasefoldName(account)
if err != nil { if err != nil {
@ -286,14 +377,14 @@ func (am *AccountManager) serializeCredentials(passphrase string, certfp string)
bcryptCost := int(am.server.Config().Accounts.Registration.BcryptCost) bcryptCost := int(am.server.Config().Accounts.Registration.BcryptCost)
creds.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost) creds.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost)
if err != nil { if err != nil {
am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err)) am.server.logger.Error("internal", "could not hash password", err.Error())
return "", errAccountCreation return "", errAccountCreation
} }
} }
credText, err := json.Marshal(creds) credText, err := json.Marshal(creds)
if err != nil { if err != nil {
am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err)) am.server.logger.Error("internal", "could not marshal credentials", err.Error())
return "", errAccountCreation return "", errAccountCreation
} }
return string(credText), nil return string(credText), nil
@ -367,7 +458,7 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou
// config.TLS.InsecureSkipVerify // config.TLS.InsecureSkipVerify
err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message) err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
if err != nil { if err != nil {
am.server.logger.Error("internal", fmt.Sprintf("Failed to dispatch e-mail: %v", err)) am.server.logger.Error("internal", "Failed to dispatch e-mail", err.Error())
} }
return return
} }
@ -576,7 +667,9 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
case 0: case 0:
err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase) err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase)
case 1: case 1:
err = passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil {
err = errAccountInvalidCredentials
}
default: default:
err = errAccountInvalidCredentials err = errAccountInvalidCredentials
} }
@ -619,7 +712,7 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl
result.RegisteredAt = time.Unix(regTimeInt, 0) result.RegisteredAt = time.Unix(regTimeInt, 0)
e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials) e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
if e != nil { if e != nil {
am.server.logger.Error("internal", fmt.Sprintf("could not unmarshal credentials: %v", e)) am.server.logger.Error("internal", "could not unmarshal credentials", e.Error())
err = errAccountDoesNotExist err = errAccountDoesNotExist
return return
} }
@ -628,7 +721,7 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl
if raw.VHost != "" { if raw.VHost != "" {
e := json.Unmarshal([]byte(raw.VHost), &result.VHost) e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
if e != nil { if e != nil {
am.server.logger.Warning("internal", fmt.Sprintf("could not unmarshal vhost for account %s: %v", result.Name, e)) am.server.logger.Warning("internal", "could not unmarshal vhost for account", result.Name, e.Error())
// pretend they have no vhost and move on // pretend they have no vhost and move on
} }
} }
@ -992,10 +1085,12 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo)
func (am *AccountManager) Login(client *Client, account ClientAccount) { func (am *AccountManager) Login(client *Client, account ClientAccount) {
changed := client.SetAccountName(account.Name) changed := client.SetAccountName(account.Name)
if changed { if !changed {
go client.nickTimer.Touch() return
} }
client.nickTimer.Touch()
am.applyVHostInfo(client, account.VHost) am.applyVHostInfo(client, account.VHost)
casefoldedAccount := client.Account() casefoldedAccount := client.Account()

View File

@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"time" "time"
"sync" "sync"
@ -48,7 +49,7 @@ type Channel struct {
func NewChannel(s *Server, name string, regInfo *RegisteredChannel) *Channel { func NewChannel(s *Server, name string, regInfo *RegisteredChannel) *Channel {
casefoldedName, err := CasefoldChannel(name) casefoldedName, err := CasefoldChannel(name)
if err != nil { if err != nil {
s.logger.Error("internal", fmt.Sprintf("Bad channel name %s: %v", name, err)) s.logger.Error("internal", "Bad channel name", name, err.Error())
return nil return nil
} }
@ -346,8 +347,7 @@ func (channel *Channel) IsEmpty() bool {
// Join joins the given client to this channel (if they can be joined). // Join joins the given client to this channel (if they can be joined).
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) { func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
account := client.Account() details := client.Details()
nickMaskCasefolded := client.NickMaskCasefolded()
channel.stateMutex.RLock() channel.stateMutex.RLock()
chname := channel.name chname := channel.name
@ -357,7 +357,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
limit := channel.userLimit limit := channel.userLimit
chcount := len(channel.members) chcount := len(channel.members)
_, alreadyJoined := channel.members[client] _, alreadyJoined := channel.members[client]
persistentMode := channel.accountToUMode[account] persistentMode := channel.accountToUMode[details.account]
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
if alreadyJoined { if alreadyJoined {
@ -367,7 +367,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
// the founder can always join (even if they disabled auto +q on join); // the founder can always join (even if they disabled auto +q on join);
// anyone who automatically receives halfop or higher can always join // anyone who automatically receives halfop or higher can always join
hasPrivs := isSajoin || (founder != "" && founder == account) || (persistentMode != 0 && persistentMode != modes.Voice) hasPrivs := isSajoin || (founder != "" && founder == details.account) || (persistentMode != 0 && persistentMode != modes.Voice)
if !hasPrivs && limit != 0 && chcount >= limit { if !hasPrivs && limit != 0 && chcount >= limit {
rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l")) rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
@ -379,20 +379,20 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
return return
} }
isInvited := client.CheckInvited(chcfname) || channel.lists[modes.InviteMask].Match(nickMaskCasefolded) isInvited := client.CheckInvited(chcfname) || channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded)
if !hasPrivs && channel.flags.HasMode(modes.InviteOnly) && !isInvited { if !hasPrivs && channel.flags.HasMode(modes.InviteOnly) && !isInvited {
rb.Add(nil, client.server.name, ERR_INVITEONLYCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "i")) rb.Add(nil, client.server.name, ERR_INVITEONLYCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "i"))
return return
} }
if !hasPrivs && channel.lists[modes.BanMask].Match(nickMaskCasefolded) && if !hasPrivs && channel.lists[modes.BanMask].Match(details.nickMaskCasefolded) &&
!isInvited && !isInvited &&
!channel.lists[modes.ExceptMask].Match(nickMaskCasefolded) { !channel.lists[modes.ExceptMask].Match(details.nickMaskCasefolded) {
rb.Add(nil, client.server.name, ERR_BANNEDFROMCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "b")) rb.Add(nil, client.server.name, ERR_BANNEDFROMCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "b"))
return return
} }
client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", client.nick, chname)) client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
newChannel, givenMode := func() (newChannel bool, givenMode modes.Mode) { newChannel, givenMode := func() (newChannel bool, givenMode modes.Mode) {
channel.joinPartMutex.Lock() channel.joinPartMutex.Lock()
@ -416,15 +416,19 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
}() }()
channel.regenerateMembersCache() channel.regenerateMembersCache()
channel.history.Add(history.Item{
Type: history.Join,
Nick: details.nickMask,
AccountName: details.accountName,
Msgid: details.realname,
})
return return
}() }()
client.addChannel(channel) client.addChannel(channel)
nick := client.Nick()
nickmask := client.NickMaskString()
realname := client.Realname()
accountName := client.AccountName()
var modestr string var modestr string
if givenMode != 0 { if givenMode != 0 {
modestr = fmt.Sprintf("+%v", givenMode) modestr = fmt.Sprintf("+%v", givenMode)
@ -435,19 +439,19 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
continue continue
} }
if member.capabilities.Has(caps.ExtendedJoin) { if member.capabilities.Has(caps.ExtendedJoin) {
member.Send(nil, nickmask, "JOIN", chname, accountName, realname) member.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
} else { } else {
member.Send(nil, nickmask, "JOIN", chname) member.Send(nil, details.nickMask, "JOIN", chname)
} }
if givenMode != 0 { if givenMode != 0 {
member.Send(nil, client.server.name, "MODE", chname, modestr, nick) member.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
} }
} }
if client.capabilities.Has(caps.ExtendedJoin) { if client.capabilities.Has(caps.ExtendedJoin) {
rb.Add(nil, nickmask, "JOIN", chname, accountName, realname) rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
} else { } else {
rb.Add(nil, nickmask, "JOIN", chname) rb.Add(nil, details.nickMask, "JOIN", chname)
} }
// don't send topic when it's an entirely new channel // don't send topic when it's an entirely new channel
@ -458,16 +462,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
channel.Names(client, rb) channel.Names(client, rb)
if givenMode != 0 { if givenMode != 0 {
rb.Add(nil, client.server.name, "MODE", chname, modestr, nick) rb.Add(nil, client.server.name, "MODE", chname, modestr, details.nick)
} }
channel.history.Add(history.Item{
Type: history.Join,
Nick: nickmask,
AccountName: accountName,
Msgid: realname,
})
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex // TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
rb.Flush(true) rb.Flush(true)
@ -489,20 +486,20 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
channel.Quit(client) channel.Quit(client)
nickmask := client.NickMaskString() details := client.Details()
for _, member := range channel.Members() { for _, member := range channel.Members() {
member.Send(nil, nickmask, "PART", chname, message) member.Send(nil, details.nickMask, "PART", chname, message)
} }
rb.Add(nil, nickmask, "PART", chname, message) rb.Add(nil, details.nickMask, "PART", chname, message)
channel.history.Add(history.Item{ channel.history.Add(history.Item{
Type: history.Part, Type: history.Part,
Nick: nickmask, Nick: details.nickMask,
AccountName: client.AccountName(), AccountName: details.accountName,
Message: utils.MakeSplitMessage(message, true), Message: utils.MakeSplitMessage(message, true),
}) })
client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", client.nick, chname)) client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname))
} }
// Resume is called after a successful global resume to: // Resume is called after a successful global resume to:
@ -591,10 +588,17 @@ func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Tim
rb.Send(true) rb.Send(true)
} }
func stripMaskFromNick(nickMask string) (nick string) {
index := strings.Index(nickMask, "!")
if index == -1 {
return
}
return nickMask[0:index]
}
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) { func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
chname := channel.Name() chname := channel.Name()
client := rb.target client := rb.target
extendedJoin := client.capabilities.Has(caps.ExtendedJoin)
serverTime := client.capabilities.Has(caps.ServerTime) serverTime := client.capabilities.Has(caps.ServerTime)
for _, item := range items { for _, item := range items {
@ -609,21 +613,27 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
case history.Notice: case history.Notice:
rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message) rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message)
case history.Join: case history.Join:
if extendedJoin { nick := stripMaskFromNick(item.Nick)
// XXX Msgid is the realname in this case var message string
rb.Add(tags, item.Nick, "JOIN", chname, item.AccountName, item.Msgid) if item.AccountName == "*" {
message = fmt.Sprintf(client.t("%s joined the channel"), nick)
} else { } else {
rb.Add(tags, item.Nick, "JOIN", chname) message = fmt.Sprintf(client.t("%s [account: %s] joined the channel"), nick, item.AccountName)
} }
case history.Quit: rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
// XXX: send QUIT as PART to avoid having to correctly deduplicate and synchronize
// QUIT messages across channels
fallthrough
case history.Part: case history.Part:
rb.Add(tags, item.Nick, "PART", chname, item.Message.Original) nick := stripMaskFromNick(item.Nick)
message := fmt.Sprintf(client.t("%s left the channel (%s)"), nick, item.Message.Original)
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
case history.Quit:
nick := stripMaskFromNick(item.Nick)
message := fmt.Sprintf(client.t("%s quit (%s)"), nick, item.Message.Original)
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
case history.Kick: case history.Kick:
nick := stripMaskFromNick(item.Nick)
// XXX Msgid is the kick target // XXX Msgid is the kick target
rb.Add(tags, item.Nick, "KICK", chname, item.Msgid, item.Message.Original) message := fmt.Sprintf(client.t("%s kicked %s (%s)"), nick, item.Msgid, item.Message.Original)
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
} }
} }
} }

View File

@ -19,6 +19,7 @@ import (
"github.com/goshuirc/irc-go/ircmsg" "github.com/goshuirc/irc-go/ircmsg"
ident "github.com/oragono/go-ident" ident "github.com/oragono/go-ident"
"github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/caps"
"github.com/oragono/oragono/irc/connection_limits"
"github.com/oragono/oragono/irc/history" "github.com/oragono/oragono/irc/history"
"github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/modes"
"github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/sno"
@ -52,7 +53,7 @@ type ResumeDetails struct {
// Client is an IRC client. // Client is an IRC client.
type Client struct { type Client struct {
account string account string
accountName string accountName string // display name of the account: uncasefolded, '*' if not logged in
atime time.Time atime time.Time
authorized bool authorized bool
awayMessage string awayMessage string
@ -73,6 +74,7 @@ type Client struct {
isDestroyed bool isDestroyed bool
isQuitting bool isQuitting bool
languages []string languages []string
loginThrottle connection_limits.GenericThrottle
maxlenTags uint32 maxlenTags uint32
maxlenRest uint32 maxlenRest uint32
nick string nick string
@ -100,6 +102,25 @@ type Client struct {
history *history.Buffer history *history.Buffer
} }
// WhoWas is the subset of client details needed to answer a WHOWAS query
type WhoWas struct {
nick string
nickCasefolded string
username string
hostname string
realname string
}
// ClientDetails is a standard set of details about a client
type ClientDetails struct {
WhoWas
nickMask string
nickMaskCasefolded string
account string
accountName string
}
// NewClient sets up a new client and starts its goroutine. // NewClient sets up a new client and starts its goroutine.
func NewClient(server *Server, conn net.Conn, isTLS bool) { func NewClient(server *Server, conn net.Conn, isTLS bool) {
now := time.Now() now := time.Now()
@ -115,8 +136,13 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) {
channels: make(ChannelSet), channels: make(ChannelSet),
ctime: now, ctime: now,
flags: modes.NewModeSet(), flags: modes.NewModeSet(),
loginThrottle: connection_limits.GenericThrottle{
Duration: config.Accounts.LoginThrottling.Duration,
Limit: config.Accounts.LoginThrottling.MaxAttempts,
},
server: server, server: server,
socket: socket, socket: socket,
accountName: "*",
nick: "*", // * is used until actual nick is given nick: "*", // * is used until actual nick is given
nickCasefolded: "*", nickCasefolded: "*",
nickMaskString: "*", // * is used until actual nick is given nickMaskString: "*", // * is used until actual nick is given

View File

@ -72,7 +72,7 @@ func (clients *ClientManager) removeInternal(client *Client) (err error) {
delete(clients.byNick, oldcfnick) delete(clients.byNick, oldcfnick)
} else { } else {
// this shouldn't happen, but we can ignore it // this shouldn't happen, but we can ignore it
client.server.logger.Warning("internal", fmt.Sprintf("clients for nick %s out of sync", oldcfnick)) client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick)
err = errNickMissing err = errNickMissing
} }
} }
@ -119,17 +119,11 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
return err return err
} }
var reservedAccount string reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick)
var method NickReservationMethod
if client.server.AccountConfig().NickReservation.Enabled {
reservedAccount = client.server.accounts.NickToAccount(newcfnick)
method = client.server.AccountConfig().NickReservation.Method
}
clients.Lock() clients.Lock()
defer clients.Unlock() defer clients.Unlock()
clients.removeInternal(client)
currentNewEntry := clients.byNick[newcfnick] currentNewEntry := clients.byNick[newcfnick]
// the client may just be changing case // the client may just be changing case
if currentNewEntry != nil && currentNewEntry != client { if currentNewEntry != nil && currentNewEntry != client {
@ -138,6 +132,7 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
if method == NickReservationStrict && reservedAccount != client.Account() { if method == NickReservationStrict && reservedAccount != client.Account() {
return errNicknameReserved return errNicknameReserved
} }
clients.removeInternal(client)
clients.byNick[newcfnick] = client clients.byNick[newcfnick] = client
client.updateNickMask(newNick) client.updateNickMask(newNick)
return nil return nil

View File

@ -8,7 +8,6 @@ package irc
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -55,6 +54,11 @@ func (conf *TLSListenConfig) Config() (*tls.Config, error) {
type AccountConfig struct { type AccountConfig struct {
Registration AccountRegistrationConfig Registration AccountRegistrationConfig
AuthenticationEnabled bool `yaml:"authentication-enabled"` AuthenticationEnabled bool `yaml:"authentication-enabled"`
LoginThrottling struct {
Enabled bool
Duration time.Duration
MaxAttempts int `yaml:"max-attempts"`
} `yaml:"login-throttling"`
SkipServerPassword bool `yaml:"skip-server-password"` SkipServerPassword bool `yaml:"skip-server-password"`
NickReservation NickReservationConfig `yaml:"nick-reservation"` NickReservation NickReservationConfig `yaml:"nick-reservation"`
VHosts VHostConfig VHosts VHostConfig
@ -100,10 +104,50 @@ type VHostConfig struct {
type NickReservationMethod int type NickReservationMethod int
const ( const (
NickReservationWithTimeout NickReservationMethod = iota // NickReservationOptional is the zero value; it serializes to
// "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`.
// in both cases, it means "defer to the other source of truth", i.e.,
// in the config, defer to the user's custom setting, and as a custom setting,
// defer to the default in the config. if both are NickReservationOptional then
// there is no enforcement.
NickReservationOptional NickReservationMethod = iota
NickReservationNone
NickReservationWithTimeout
NickReservationStrict NickReservationStrict
) )
func nickReservationToString(method NickReservationMethod) string {
switch method {
case NickReservationOptional:
return "default"
case NickReservationNone:
return "none"
case NickReservationWithTimeout:
return "timeout"
case NickReservationStrict:
return "strict"
default:
return ""
}
}
func nickReservationFromString(method string) (NickReservationMethod, error) {
switch method {
case "default":
return NickReservationOptional, nil
case "optional":
return NickReservationOptional, nil
case "none":
return NickReservationNone, nil
case "timeout":
return NickReservationWithTimeout, nil
case "strict":
return NickReservationStrict, nil
default:
return NickReservationOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
}
}
func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error { func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
var orig, raw string var orig, raw string
var err error var err error
@ -113,20 +157,18 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
if raw, err = Casefold(orig); err != nil { if raw, err = Casefold(orig); err != nil {
return err return err
} }
if raw == "timeout" { method, err := nickReservationFromString(raw)
*nr = NickReservationWithTimeout if err == nil {
} else if raw == "strict" { *nr = method
*nr = NickReservationStrict
} else {
return errors.New(fmt.Sprintf("invalid nick-reservation.method value: %s", orig))
} }
return nil return err
} }
type NickReservationConfig struct { type NickReservationConfig struct {
Enabled bool Enabled bool
AdditionalNickLimit int `yaml:"additional-nick-limit"` AdditionalNickLimit int `yaml:"additional-nick-limit"`
Method NickReservationMethod Method NickReservationMethod
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
RenameTimeout time.Duration `yaml:"rename-timeout"` RenameTimeout time.Duration `yaml:"rename-timeout"`
RenamePrefix string `yaml:"rename-prefix"` RenamePrefix string `yaml:"rename-prefix"`
} }
@ -558,6 +600,10 @@ func LoadConfig(filename string) (config *Config, err error) {
config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
} }
if !config.Accounts.LoginThrottling.Enabled {
config.Accounts.LoginThrottling.MaxAttempts = 0 // limit of 0 means disabled
}
maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString) maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
if err != nil { if err != nil {
return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error()) return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())

View File

@ -27,7 +27,44 @@ type ThrottlerConfig struct {
// ThrottleDetails holds the connection-throttling details for a subnet/IP. // ThrottleDetails holds the connection-throttling details for a subnet/IP.
type ThrottleDetails struct { type ThrottleDetails struct {
Start time.Time Start time.Time
ClientCount int Count int
}
// GenericThrottle allows enforcing limits of the form
// "at most X events per time window of duration Y"
type GenericThrottle struct {
ThrottleDetails // variable state: what events have been seen
// these are constant after creation:
Duration time.Duration // window length to consider
Limit int // number of events allowed per window
}
// Touch checks whether an additional event is allowed:
// it either denies it (by returning false) or allows it (by returning true)
// and records it
func (g *GenericThrottle) Touch() (throttled bool, remainingTime time.Duration) {
return g.touch(time.Now())
}
func (g *GenericThrottle) touch(now time.Time) (throttled bool, remainingTime time.Duration) {
if g.Limit == 0 {
return // limit of 0 disables throttling
}
elapsed := now.Sub(g.Start)
if elapsed > g.Duration {
// reset window, record the operation
g.Start = now
g.Count = 1
return false, 0
} else if g.Count >= g.Limit {
// we are throttled
return true, g.Start.Add(g.Duration).Sub(now)
} else {
// we are not throttled, record the operation
g.Count += 1
return false, 0
}
} }
// Throttler manages automated client connection throttling. // Throttler manages automated client connection throttling.
@ -102,21 +139,21 @@ func (ct *Throttler) AddClient(addr net.IP) error {
ct.maskAddr(addr) ct.maskAddr(addr)
addrString := addr.String() addrString := addr.String()
details, exists := ct.population[addrString] details := ct.population[addrString] // retrieve mutable throttle state from the map
if !exists || details.Start.Add(ct.duration).Before(time.Now()) { // add in constant state to process the limiting operation
details = ThrottleDetails{ g := GenericThrottle{
Start: time.Now(), ThrottleDetails: details,
} Duration: ct.duration,
Limit: ct.subnetLimit,
} }
throttled, _ := g.Touch() // actually check the limit
ct.population[addrString] = g.ThrottleDetails // store modified mutable state
if details.ClientCount+1 > ct.subnetLimit { if throttled {
return errTooManyClients return errTooManyClients
} } else {
details.ClientCount++
ct.population[addrString] = details
return nil return nil
}
} }
func (ct *Throttler) BanDuration() time.Duration { func (ct *Throttler) BanDuration() time.Duration {

View File

@ -0,0 +1,86 @@
// Copyright (c) 2018 Shivaram Lingamneni
// released under the MIT license
package connection_limits
import (
"net"
"reflect"
"testing"
"time"
)
func assertEqual(supplied, expected interface{}, t *testing.T) {
if !reflect.DeepEqual(supplied, expected) {
t.Errorf("expected %v but got %v", expected, supplied)
}
}
func TestGenericThrottle(t *testing.T) {
minute, _ := time.ParseDuration("1m")
second, _ := time.ParseDuration("1s")
zero, _ := time.ParseDuration("0s")
throttler := GenericThrottle{
Duration: minute,
Limit: 2,
}
now := time.Now()
throttled, remaining := throttler.touch(now)
assertEqual(throttled, false, t)
assertEqual(remaining, zero, t)
now = now.Add(second)
throttled, remaining = throttler.touch(now)
assertEqual(throttled, false, t)
assertEqual(remaining, zero, t)
now = now.Add(second)
throttled, remaining = throttler.touch(now)
assertEqual(throttled, true, t)
assertEqual(remaining, 58*second, t)
now = now.Add(minute)
throttled, remaining = throttler.touch(now)
assertEqual(throttled, false, t)
assertEqual(remaining, zero, t)
}
func TestGenericThrottleDisabled(t *testing.T) {
minute, _ := time.ParseDuration("1m")
throttler := GenericThrottle{
Duration: minute,
Limit: 0,
}
for i := 0; i < 1024; i += 1 {
throttled, _ := throttler.Touch()
if throttled {
t.Error("disabled throttler should not throttle")
}
}
}
func TestConnectionThrottle(t *testing.T) {
minute, _ := time.ParseDuration("1m")
maxConnections := 3
config := ThrottlerConfig{
Enabled: true,
CidrLenIPv4: 32,
CidrLenIPv6: 64,
ConnectionsPerCidr: maxConnections,
Duration: minute,
}
throttler := NewThrottler()
throttler.ApplyConfig(config)
addr := net.ParseIP("8.8.8.8")
for i := 0; i < maxConnections; i += 1 {
err := throttler.AddClient(addr)
assertEqual(err, nil, t)
}
err := throttler.AddClient(addr)
assertEqual(err, errTooManyClients, t)
}

View File

@ -40,6 +40,7 @@ var (
errSaslFail = errors.New("SASL failed") errSaslFail = errors.New("SASL failed")
errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token") errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token")
errInvalidUsername = errors.New("Invalid username") errInvalidUsername = errors.New("Invalid username")
errFeatureDisabled = errors.New("That feature is disabled")
) )
// Socket Errors // Socket Errors

View File

@ -147,9 +147,6 @@ func (client *Client) Account() string {
func (client *Client) AccountName() string { func (client *Client) AccountName() string {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
if client.accountName == "" {
return "*"
}
return client.accountName return client.accountName
} }
@ -182,18 +179,6 @@ func (client *Client) SetAuthorized(authorized bool) {
client.authorized = authorized client.authorized = authorized
} }
func (client *Client) PreregNick() string {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
return client.preregNick
}
func (client *Client) SetPreregNick(preregNick string) {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
client.preregNick = preregNick
}
func (client *Client) HasMode(mode modes.Mode) bool { func (client *Client) HasMode(mode modes.Mode) bool {
// client.flags has its own synch // client.flags has its own synch
return client.flags.HasMode(mode) return client.flags.HasMode(mode)
@ -217,15 +202,22 @@ func (client *Client) Channels() (result []*Channel) {
} }
func (client *Client) WhoWas() (result WhoWas) { func (client *Client) WhoWas() (result WhoWas) {
return client.Details().WhoWas
}
func (client *Client) Details() (result ClientDetails) {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
result.nicknameCasefolded = client.nickCasefolded result.nick = client.nick
result.nickname = client.nick result.nickCasefolded = client.nickCasefolded
result.username = client.username result.username = client.username
result.hostname = client.hostname result.hostname = client.username
result.realname = client.realname result.realname = client.realname
result.nickMask = client.nickMaskString
result.nickMaskCasefolded = client.nickMaskCasefolded
result.account = client.account
result.accountName = client.accountName
return return
} }

View File

@ -83,9 +83,10 @@ func parseCallback(spec string, config *AccountConfig) (callbackNamespace string
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential> // ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
nick := client.Nick()
// clients can't reg new accounts if they're already logged in // clients can't reg new accounts if they're already logged in
if client.LoggedIntoAccount() { if client.LoggedIntoAccount() {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("You're already logged into an account")) rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, "*", client.t("You're already logged into an account"))
return false return false
} }
@ -94,12 +95,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
casefoldedAccount, err := CasefoldName(account) casefoldedAccount, err := CasefoldName(account)
// probably don't need explicit check for "*" here... but let's do it anyway just to make sure // probably don't need explicit check for "*" here... but let's do it anyway just to make sure
if err != nil || msg.Params[1] == "*" { 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")) rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, account, client.t("Account name is not valid"))
return false return false
} }
if len(msg.Params) < 4 { if len(msg.Params) < 4 {
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters")) rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, nick, msg.Command, client.t("Not enough parameters"))
return false return false
} }
@ -107,7 +108,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig()) callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
if callbackNamespace == "" { if callbackNamespace == "" {
rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackSpec, client.t("Callback namespace is not supported")) rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, nick, account, callbackSpec, client.t("Callback namespace is not supported"))
return false return false
} }
@ -131,12 +132,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
} }
} }
if credentialType == "certfp" && client.certfp == "" { 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")) rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
return false return false
} }
if !credentialValid { if !credentialValid {
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("Credential type is not supported")) rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
return false return false
} }
@ -146,6 +147,13 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
} else if credentialType == "passphrase" { } else if credentialType == "passphrase" {
passphrase = credentialValue passphrase = credentialValue
} }
throttled, remainingTime := client.loginThrottle.Touch()
if throttled {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
return false
}
err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp) err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
if err != nil { if err != nil {
msg := "Unknown" msg := "Unknown"
@ -161,7 +169,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists { if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
msg = err.Error() msg = err.Error()
} }
rb.Add(nil, server.name, code, client.nick, "ACC", "REGISTER", client.t(msg)) rb.Add(nil, server.name, code, nick, "ACC", "REGISTER", client.t(msg))
return false return false
} }
@ -175,7 +183,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
} else { } else {
messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s") messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue) message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, client.nick, casefoldedAccount, message) rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, nick, casefoldedAccount, message)
} }
return false return false
@ -336,6 +344,8 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
var accountKey, authzid string var accountKey, authzid string
nick := client.Nick()
if len(splitValue) == 3 { if len(splitValue) == 3 {
accountKey = string(splitValue[0]) accountKey = string(splitValue[0])
authzid = string(splitValue[1]) authzid = string(splitValue[1])
@ -343,11 +353,17 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
if accountKey == "" { if accountKey == "" {
accountKey = authzid accountKey = authzid
} else if accountKey != authzid { } else if accountKey != authzid {
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: authcid and authzid should be the same")) rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
return false return false
} }
} else { } else {
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: Invalid auth blob")) rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: Invalid auth blob"))
return false
}
throttled, remainingTime := client.loginThrottle.Touch()
if throttled {
rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
return false return false
} }
@ -355,7 +371,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
err := server.accounts.AuthenticateByPassphrase(client, accountKey, password) err := server.accounts.AuthenticateByPassphrase(client, accountKey, password)
if err != nil { if err != nil {
msg := authErrorToMessage(server, err) 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))) rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
return false return false
} }
@ -367,7 +383,7 @@ func authErrorToMessage(server *Server, err error) (msg string) {
if err == errAccountDoesNotExist || err == errAccountUnverified || err == errAccountInvalidCredentials { if err == errAccountDoesNotExist || err == errAccountUnverified || err == errAccountInvalidCredentials {
msg = err.Error() msg = err.Error()
} else { } else {
server.logger.Error("internal", fmt.Sprintf("sasl authentication failure: %v", err)) server.logger.Error("internal", "sasl authentication failure", err.Error())
msg = "Unknown" msg = "Unknown"
} }
return return
@ -1646,7 +1662,7 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
if client.Registered() { if client.Registered() {
performNickChange(server, client, client, msg.Params[0], rb) performNickChange(server, client, client, msg.Params[0], rb)
} else { } else {
client.SetPreregNick(msg.Params[0]) client.preregNick = msg.Params[0]
} }
return false return false
} }
@ -2541,7 +2557,7 @@ func whowasHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
} }
} else { } else {
for _, whoWas := range results { for _, whoWas := range results {
rb.Add(nil, server.name, RPL_WHOWASUSER, cnick, whoWas.nickname, whoWas.username, whoWas.hostname, "*", whoWas.realname) rb.Add(nil, server.name, RPL_WHOWASUSER, cnick, whoWas.nick, whoWas.username, whoWas.hostname, "*", whoWas.realname)
} }
} }
if len(nickname) > 0 { if len(nickname) > 0 {

View File

@ -189,14 +189,14 @@ type NickTimer struct {
// NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled) // NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled)
func NewNickTimer(client *Client) *NickTimer { func NewNickTimer(client *Client) *NickTimer {
config := client.server.AccountConfig().NickReservation config := client.server.AccountConfig().NickReservation
if !(config.Enabled && config.Method == NickReservationWithTimeout) { if !(config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)) {
return nil return nil
} }
nt := NickTimer{
return &NickTimer{
client: client, client: client,
timeout: config.RenameTimeout, timeout: config.RenameTimeout,
} }
return &nt
} }
// Touch records a nick change and updates the timer as necessary // Touch records a nick change and updates the timer as necessary
@ -207,7 +207,8 @@ func (nt *NickTimer) Touch() {
nick := nt.client.NickCasefolded() nick := nt.client.NickCasefolded()
account := nt.client.Account() account := nt.client.Account()
accountForNick := nt.client.server.accounts.NickToAccount(nick) accountForNick, method := nt.client.server.accounts.EnforcementStatus(nick)
enforceTimeout := method == NickReservationWithTimeout
var shouldWarn bool var shouldWarn bool
@ -227,11 +228,11 @@ func (nt *NickTimer) Touch() {
nt.accountForNick = accountForNick nt.accountForNick = accountForNick
delinquent := accountForNick != "" && accountForNick != account delinquent := accountForNick != "" && accountForNick != account
if nt.timer != nil && (!delinquent || accountChanged) { if nt.timer != nil && (!enforceTimeout || !delinquent || accountChanged) {
nt.timer.Stop() nt.timer.Stop()
nt.timer = nil nt.timer = nil
} }
if delinquent && accountChanged { if enforceTimeout && delinquent && accountChanged {
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout) nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
shouldWarn = true shouldWarn = true
} }

View File

@ -5,6 +5,7 @@ package logger
import ( import (
"bufio" "bufio"
"bytes"
"fmt" "fmt"
"os" "os"
"time" "time"
@ -32,6 +33,20 @@ const (
LogError LogError
) )
var (
colorTimeGrey = ansi.ColorFunc("243")
colorGrey = ansi.ColorFunc("8")
colorAlert = ansi.ColorFunc("232+b:red")
colorWarn = ansi.ColorFunc("black:214")
colorInfo = ansi.ColorFunc("117")
colorDebug = ansi.ColorFunc("78")
colorSection = ansi.ColorFunc("229")
separator = colorGrey(":")
colorableStdout = colorable.NewColorableStdout()
colorableStderr = colorable.NewColorableStderr()
)
var ( var (
// LogLevelNames takes a config name and gives the real log level. // LogLevelNames takes a config name and gives the real log level.
LogLevelNames = map[string]Level{ LogLevelNames = map[string]Level{
@ -230,51 +245,56 @@ func (logger *singleLogger) Log(level Level, logType string, messageParts ...str
} }
// assemble full line // assemble full line
timeGrey := ansi.ColorFunc("243")
grey := ansi.ColorFunc("8")
alert := ansi.ColorFunc("232+b:red")
warn := ansi.ColorFunc("black:214")
info := ansi.ColorFunc("117")
debug := ansi.ColorFunc("78")
section := ansi.ColorFunc("229")
levelDisplay := LogLevelDisplayNames[level] levelDisplay := LogLevelDisplayNames[level]
if level == LogError { if level == LogError {
levelDisplay = alert(levelDisplay) levelDisplay = colorAlert(levelDisplay)
} else if level == LogWarning { } else if level == LogWarning {
levelDisplay = warn(levelDisplay) levelDisplay = colorWarn(levelDisplay)
} else if level == LogInfo { } else if level == LogInfo {
levelDisplay = info(levelDisplay) levelDisplay = colorInfo(levelDisplay)
} else if level == LogDebug { } else if level == LogDebug {
levelDisplay = debug(levelDisplay) levelDisplay = colorDebug(levelDisplay)
} }
sep := grey(":") var formattedBuf, rawBuf bytes.Buffer
fullStringFormatted := fmt.Sprintf("%s %s %s %s %s %s ", timeGrey(time.Now().UTC().Format("2006-01-02T15:04:05.000Z")), sep, levelDisplay, sep, section(logType), sep) fmt.Fprintf(&formattedBuf, "%s %s %s %s %s %s ", colorTimeGrey(time.Now().UTC().Format("2006-01-02T15:04:05.000Z")), separator, levelDisplay, separator, colorSection(logType), separator)
fullStringRaw := fmt.Sprintf("%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType) if logger.MethodFile.Enabled {
for i, p := range messageParts { fmt.Fprintf(&rawBuf, "%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType)
fullStringFormatted += p
fullStringRaw += p
if i != len(messageParts)-1 {
fullStringFormatted += " " + sep + " "
fullStringRaw += " : "
} }
for i, p := range messageParts {
formattedBuf.WriteString(p)
if logger.MethodFile.Enabled {
rawBuf.WriteString(p)
}
if i != len(messageParts)-1 {
formattedBuf.WriteRune(' ')
formattedBuf.WriteString(separator)
formattedBuf.WriteRune(' ')
if logger.MethodFile.Enabled {
rawBuf.WriteString(" : ")
}
}
}
formattedBuf.WriteRune('\n')
if logger.MethodFile.Enabled {
rawBuf.WriteRune('\n')
} }
// output // output
if logger.MethodSTDOUT { if logger.MethodSTDOUT {
logger.stdoutWriteLock.Lock() logger.stdoutWriteLock.Lock()
fmt.Fprintln(colorable.NewColorableStdout(), fullStringFormatted) colorableStdout.Write(formattedBuf.Bytes())
logger.stdoutWriteLock.Unlock() logger.stdoutWriteLock.Unlock()
} }
if logger.MethodSTDERR { if logger.MethodSTDERR {
logger.stdoutWriteLock.Lock() logger.stdoutWriteLock.Lock()
fmt.Fprintln(colorable.NewColorableStderr(), fullStringFormatted) colorableStderr.Write(formattedBuf.Bytes())
logger.stdoutWriteLock.Unlock() logger.stdoutWriteLock.Unlock()
} }
if logger.MethodFile.Enabled { if logger.MethodFile.Enabled {
logger.fileWriteLock.Lock() logger.fileWriteLock.Lock()
logger.MethodFile.Writer.WriteString(fullStringRaw + "\n") logger.MethodFile.Writer.Write(rawBuf.Bytes())
logger.MethodFile.Writer.Flush() logger.MethodFile.Writer.Flush()
logger.fileWriteLock.Unlock() logger.fileWriteLock.Unlock()
} }

View File

@ -59,7 +59,7 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick)) client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
if hadNick { if hadNick {
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nickname, nickname)) target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname))
target.server.whoWas.Append(whowas) target.server.whoWas.Append(whowas)
for friend := range target.Friends() { for friend := range target.Friends() {
friend.Send(nil, origNickMask, "NICK", nickname) friend.Send(nil, origNickMask, "NICK", nickname)

View File

@ -25,6 +25,11 @@ func nsGroupEnabled(server *Server) bool {
return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled
} }
func nsEnforceEnabled(server *Server) bool {
config := server.Config()
return config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement
}
const nickservHelp = `NickServ lets you register and login to an account. const nickservHelp = `NickServ lets you register and login to an account.
To see in-depth help for a specific NickServ command, try: To see in-depth help for a specific NickServ command, try:
@ -44,6 +49,22 @@ DROP de-links the given (or your current) nickname from your user account.`,
enabled: servCmdRequiresAccreg, enabled: servCmdRequiresAccreg,
authRequired: true, authRequired: true,
}, },
"enforce": {
handler: nsEnforceHandler,
help: `Syntax: $bENFORCE [method]$b
ENFORCE lets you specify a custom enforcement mechanism for your registered
nicknames. Your options are:
1. 'none' [no enforcement, overriding the server default]
2. 'timeout' [anyone using the nick must authenticate before a deadline,
or else they will be renamed]
3. 'strict' [you must already be authenticated to use the nick]
4. 'default' [use the server default]
With no arguments, queries your current enforcement status.`,
helpShort: `$bENFORCE$b lets you change how your nicknames are reserved.`,
authRequired: true,
enabled: nsEnforceEnabled,
},
"ghost": { "ghost": {
handler: nsGhostHandler, handler: nsGhostHandler,
help: `Syntax: $bGHOST <nickname>$b help: `Syntax: $bGHOST <nickname>$b
@ -200,6 +221,15 @@ func nsGroupHandler(server *Server, client *Client, command, params string, rb *
} }
} }
func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) {
throttled, remainingTime := client.loginThrottle.Touch()
if throttled {
nsNotice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
return false
}
return true
}
func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
loginSuccessful := false loginSuccessful := false
@ -207,6 +237,9 @@ func nsIdentifyHandler(server *Server, client *Client, command, params string, r
// try passphrase // try passphrase
if username != "" && passphrase != "" { if username != "" && passphrase != "" {
if !nsLoginThrottleCheck(client, rb) {
return
}
err := server.accounts.AuthenticateByPassphrase(client, username, passphrase) err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
loginSuccessful = (err == nil) loginSuccessful = (err == nil)
} }
@ -407,10 +440,15 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb
var newPassword string var newPassword string
var errorMessage string var errorMessage string
hasPrivs := client.HasRoleCapabs("accreg")
if !hasPrivs && !nsLoginThrottleCheck(client, rb) {
return
}
fields := strings.Fields(params) fields := strings.Fields(params)
switch len(fields) { switch len(fields) {
case 2: case 2:
if !client.HasRoleCapabs("accreg") { if !hasPrivs {
errorMessage = "Insufficient privileges" errorMessage = "Insufficient privileges"
} else { } else {
target, newPassword = fields[0], fields[1] target, newPassword = fields[0], fields[1]
@ -443,7 +481,29 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb
if err == nil { if err == nil {
nsNotice(rb, client.t("Password changed")) nsNotice(rb, client.t("Password changed"))
} else { } else {
server.logger.Error("internal", fmt.Sprintf("could not upgrade user password: %v", err)) server.logger.Error("internal", "could not upgrade user password:", err.Error())
nsNotice(rb, client.t("Password could not be changed due to server error")) nsNotice(rb, client.t("Password could not be changed due to server error"))
} }
} }
func nsEnforceHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
arg := strings.TrimSpace(params)
if arg == "" {
status := server.accounts.getStoredEnforcementStatus(client.Account())
nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status))
} else {
method, err := nickReservationFromString(arg)
if err != nil {
nsNotice(rb, client.t("Invalid parameters"))
return
}
err = server.accounts.SetEnforcementStatus(client.Account(), method)
if err == nil {
nsNotice(rb, client.t("Enforcement method set"))
} else {
server.logger.Error("internal", "couldn't store NS ENFORCE data", err.Error())
nsNotice(rb, client.t("An error occurred"))
}
}
}

View File

@ -386,8 +386,7 @@ func (server *Server) tryRegister(c *Client) {
return return
} }
preregNick := c.PreregNick() if c.preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState {
if preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState {
return return
} }
@ -400,10 +399,10 @@ func (server *Server) tryRegister(c *Client) {
} }
rb := NewResponseBuffer(c) rb := NewResponseBuffer(c)
nickAssigned := performNickChange(server, c, c, preregNick, rb) nickAssigned := performNickChange(server, c, c, c.preregNick, rb)
rb.Send(true) rb.Send(true)
if !nickAssigned { if !nickAssigned {
c.SetPreregNick("") c.preregNick = ""
return return
} }
@ -500,9 +499,9 @@ func (client *Client) WhoisChannelsNames(target *Client) []string {
func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) { func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
cnick := client.Nick() cnick := client.Nick()
targetInfo := target.WhoWas() targetInfo := target.Details()
rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nickname, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname) rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nick, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname)
tnick := targetInfo.nickname tnick := targetInfo.nick
whoischannels := client.WhoisChannelsNames(target) whoischannels := client.WhoisChannelsNames(target)
if whoischannels != nil { if whoischannels != nil {
@ -518,9 +517,8 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
if target.HasMode(modes.TLS) { if target.HasMode(modes.TLS) {
rb.Add(nil, client.server.name, RPL_WHOISSECURE, cnick, tnick, client.t("is using a secure connection")) rb.Add(nil, client.server.name, RPL_WHOISSECURE, cnick, tnick, client.t("is using a secure connection"))
} }
taccount := target.AccountName() if targetInfo.accountName != "*" {
if taccount != "*" { rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, cnick, tnick, targetInfo.accountName, client.t("is logged in as"))
rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, cnick, tnick, taccount, client.t("is logged in as"))
} }
if target.HasMode(modes.Bot) { if target.HasMode(modes.Bot) {
rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.Config().Network.Name))) rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.Config().Network.Name)))
@ -836,7 +834,7 @@ func (server *Server) setupPprofListener(config *Config) {
} }
go func() { go func() {
if err := ps.ListenAndServe(); err != nil { if err := ps.ListenAndServe(); err != nil {
server.logger.Error("rehash", fmt.Sprintf("pprof listener failed: %v", err)) server.logger.Error("rehash", "pprof listener failed", err.Error())
} }
}() }()
server.pprofServer = &ps server.pprofServer = &ps

View File

@ -22,15 +22,6 @@ type WhoWasList struct {
accessMutex sync.RWMutex // tier 1 accessMutex sync.RWMutex // tier 1
} }
// WhoWas is an entry in the WhoWasList.
type WhoWas struct {
nicknameCasefolded string
nickname string
username string
hostname string
realname string
}
// NewWhoWasList returns a new WhoWasList // NewWhoWasList returns a new WhoWasList
func NewWhoWasList(size int) *WhoWasList { func NewWhoWasList(size int) *WhoWasList {
return &WhoWasList{ return &WhoWasList{
@ -82,7 +73,7 @@ func (list *WhoWasList) Find(nickname string, limit int) (results []WhoWas) {
// iterate backwards through the ring buffer // iterate backwards through the ring buffer
pos := list.prev(list.end) pos := list.prev(list.end)
for limit == 0 || len(results) < limit { for limit == 0 || len(results) < limit {
if casefoldedNickname == list.buffer[pos].nicknameCasefolded { if casefoldedNickname == list.buffer[pos].nickCasefolded {
results = append(results, list.buffer[pos]) results = append(results, list.buffer[pos])
} }
if pos == list.start { if pos == list.start {

View File

@ -13,8 +13,8 @@ func makeTestWhowas(nick string) WhoWas {
panic(err) panic(err)
} }
return WhoWas{ return WhoWas{
nicknameCasefolded: cfnick, nickCasefolded: cfnick,
nickname: nick, nick: nick,
username: "user", username: "user",
hostname: "oragono.io", hostname: "oragono.io",
realname: "Real Name", realname: "Real Name",
@ -36,48 +36,48 @@ func TestWhoWas(t *testing.T) {
t.Fatalf("incorrect whowas results: %v", results) t.Fatalf("incorrect whowas results: %v", results)
} }
results = wwl.Find("dan-", 10) results = wwl.Find("dan-", 10)
if len(results) != 1 || results[0].nickname != "dan-" { if len(results) != 1 || results[0].nick != "dan-" {
t.Fatalf("incorrect whowas results: %v", results) t.Fatalf("incorrect whowas results: %v", results)
} }
wwl.Append(makeTestWhowas("slingamn")) wwl.Append(makeTestWhowas("slingamn"))
results = wwl.Find("slingamN", 10) results = wwl.Find("slingamN", 10)
if len(results) != 1 || results[0].nickname != "slingamn" { if len(results) != 1 || results[0].nick != "slingamn" {
t.Fatalf("incorrect whowas results: %v", results) t.Fatalf("incorrect whowas results: %v", results)
} }
wwl.Append(makeTestWhowas("Dan-")) wwl.Append(makeTestWhowas("Dan-"))
results = wwl.Find("dan-", 10) results = wwl.Find("dan-", 10)
// reverse chronological order // reverse chronological order
if len(results) != 2 || results[0].nickname != "Dan-" || results[1].nickname != "dan-" { if len(results) != 2 || results[0].nick != "Dan-" || results[1].nick != "dan-" {
t.Fatalf("incorrect whowas results: %v", results) t.Fatalf("incorrect whowas results: %v", results)
} }
// 0 means no limit // 0 means no limit
results = wwl.Find("dan-", 0) results = wwl.Find("dan-", 0)
if len(results) != 2 || results[0].nickname != "Dan-" || results[1].nickname != "dan-" { if len(results) != 2 || results[0].nick != "Dan-" || results[1].nick != "dan-" {
t.Fatalf("incorrect whowas results: %v", results) t.Fatalf("incorrect whowas results: %v", results)
} }
// a limit of 1 should return the most recent entry only // a limit of 1 should return the most recent entry only
results = wwl.Find("dan-", 1) results = wwl.Find("dan-", 1)
if len(results) != 1 || results[0].nickname != "Dan-" { if len(results) != 1 || results[0].nick != "Dan-" {
t.Fatalf("incorrect whowas results: %v", results) t.Fatalf("incorrect whowas results: %v", results)
} }
wwl.Append(makeTestWhowas("moocow")) wwl.Append(makeTestWhowas("moocow"))
results = wwl.Find("moocow", 10) results = wwl.Find("moocow", 10)
if len(results) != 1 || results[0].nickname != "moocow" { if len(results) != 1 || results[0].nick != "moocow" {
t.Fatalf("incorrect whowas results: %v", results) t.Fatalf("incorrect whowas results: %v", results)
} }
results = wwl.Find("dan-", 10) results = wwl.Find("dan-", 10)
// should have overwritten the original entry, leaving the second // should have overwritten the original entry, leaving the second
if len(results) != 1 || results[0].nickname != "Dan-" { if len(results) != 1 || results[0].nick != "Dan-" {
t.Fatalf("incorrect whowas results: %v", results) t.Fatalf("incorrect whowas results: %v", results)
} }
// overwrite the second entry // overwrite the second entry
wwl.Append(makeTestWhowas("enckse")) wwl.Append(makeTestWhowas("enckse"))
results = wwl.Find("enckse", 10) results = wwl.Find("enckse", 10)
if len(results) != 1 || results[0].nickname != "enckse" { if len(results) != 1 || results[0].nick != "enckse" {
t.Fatalf("incorrect whowas results: %v", results) t.Fatalf("incorrect whowas results: %v", results)
} }
results = wwl.Find("slingamn", 10) results = wwl.Find("slingamn", 10)

View File

@ -179,6 +179,17 @@ accounts:
# is account authentication enabled? # is account authentication enabled?
authentication-enabled: true authentication-enabled: true
# throttle account login attempts (to prevent either password guessing, or DoS
# attacks on the server aimed at forcing repeated expensive bcrypt computations)
login-throttling:
enabled: true
# window
duration: 1m
# number of attempts allowed within the window
max-attempts: 3
# some clients (notably Pidgin and Hexchat) offer only a single password field, # some clients (notably Pidgin and Hexchat) offer only a single password field,
# which makes it impossible to specify a separate server password (for the PASS # which makes it impossible to specify a separate server password (for the PASS
# command) and SASL password. if this option is set to true, a client that # command) and SASL password. if this option is set to true, a client that
@ -195,12 +206,19 @@ accounts:
additional-nick-limit: 2 additional-nick-limit: 2
# method describes how nickname reservation is handled # method describes how nickname reservation is handled
# already logged-in using SASL or NickServ
# timeout: let the user change to the registered nickname, give them X seconds # timeout: let the user change to the registered nickname, give them X seconds
# to login and then rename them if they haven't done so # to login and then rename them if they haven't done so
# strict: don't let the user change to the registered nickname unless they're # strict: don't let the user change to the registered nickname unless they're
# already logged-in using SASL or NickServ # already logged-in using SASL or NickServ
# optional: no enforcement by default, but allow users to opt in to
# the enforcement level of their choice
method: timeout method: timeout
# allow users to set their own nickname enforcement status, e.g.,
# to opt in to strict enforcement
allow-custom-enforcement: true
# rename-timeout - this is how long users have 'til they're renamed # rename-timeout - this is how long users have 'til they're renamed
rename-timeout: 30s rename-timeout: 30s