3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-10 22:19:31 +01:00

Merge pull request #499 from slingamn/prefs.11

user preference system plus two small fixes
This commit is contained in:
Daniel Oaks 2019-05-21 08:57:57 +10:00 committed by GitHub
commit 6291a44350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 591 additions and 180 deletions

View File

@ -29,7 +29,7 @@ const (
keyAccountRegTime = "account.registered.time %s"
keyAccountCredentials = "account.credentials %s"
keyAccountAdditionalNicks = "account.additionalnicks %s"
keyAccountEnforcement = "account.customenforcement %s"
keyAccountSettings = "account.settings %s"
keyAccountVHost = "account.vhost %s"
keyCertToAccount = "account.creds.certfp %s"
keyAccountChannels = "account.channels %s"
@ -55,14 +55,14 @@ type AccountManager struct {
accountToClients map[string][]*Client
nickToAccount map[string]string
skeletonToAccount map[string]string
accountToMethod map[string]NickReservationMethod
accountToMethod map[string]NickEnforcementMethod
}
func (am *AccountManager) Initialize(server *Server) {
am.accountToClients = make(map[string][]*Client)
am.nickToAccount = make(map[string]string)
am.skeletonToAccount = make(map[string]string)
am.accountToMethod = make(map[string]NickReservationMethod)
am.accountToMethod = make(map[string]NickEnforcementMethod)
am.server = server
am.buildNickToAccountIndex()
@ -76,7 +76,7 @@ func (am *AccountManager) buildNickToAccountIndex() {
nickToAccount := make(map[string]string)
skeletonToAccount := make(map[string]string)
accountToMethod := make(map[string]NickReservationMethod)
accountToMethod := make(map[string]NickEnforcementMethod)
existsPrefix := fmt.Sprintf(keyAccountExists, "")
am.serialCacheUpdateMutex.Lock()
@ -109,12 +109,16 @@ func (am *AccountManager) buildNickToAccountIndex() {
}
}
if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil {
method, err := nickReservationFromString(methodStr)
if err == nil {
accountToMethod[account] = method
if rawPrefs, err := tx.Get(fmt.Sprintf(keyAccountSettings, account)); err == nil {
var prefs AccountSettings
err := json.Unmarshal([]byte(rawPrefs), &prefs)
if err == nil && prefs.NickEnforcement != NickEnforcementOptional {
accountToMethod[account] = prefs.NickEnforcement
} else {
am.server.logger.Error("internal", "corrupt account creds", account)
}
}
return true
})
return err
@ -180,36 +184,44 @@ func (am *AccountManager) NickToAccount(nick string) string {
return am.nickToAccount[cfnick]
}
// given an account, combine stored enforcement method with the config settings
// to compute the actual enforcement method
func configuredEnforcementMethod(config *Config, storedMethod NickEnforcementMethod) (result NickEnforcementMethod) {
if !config.Accounts.NickReservation.Enabled {
return NickEnforcementNone
}
result = storedMethod
// if they don't have a custom setting, or customization is disabled, use the default
if result == NickEnforcementOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
result = config.Accounts.NickReservation.Method
}
if result == NickEnforcementOptional {
// enforcement was explicitly enabled neither in the config or by the user
result = NickEnforcementNone
}
return
}
// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
// used to enforce ownership.
func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickReservationMethod) {
func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickEnforcementMethod) {
config := am.server.Config()
if !config.Accounts.NickReservation.Enabled {
return "", NickReservationNone
return "", NickEnforcementNone
}
am.RLock()
defer am.RUnlock()
// given an account, combine stored enforcement method with the config settings
// to compute the actual enforcement method
finalEnforcementMethod := func(account_ string) (result NickReservationMethod) {
result = am.accountToMethod[account_]
// if they don't have a custom setting, or customization is disabled, use the default
if result == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
result = config.Accounts.NickReservation.Method
}
if result == NickReservationOptional {
// enforcement was explicitly enabled neither in the config or by the user
result = NickReservationNone
}
return
finalEnforcementMethod := func(account_ string) (result NickEnforcementMethod) {
storedMethod := am.accountToMethod[account_]
return configuredEnforcementMethod(config, storedMethod)
}
nickAccount := am.nickToAccount[cfnick]
skelAccount := am.skeletonToAccount[skeleton]
if nickAccount == "" && skelAccount == "" {
return "", NickReservationNone
return "", NickEnforcementNone
} else if nickAccount != "" && (skelAccount == nickAccount || skelAccount == "") {
return nickAccount, finalEnforcementMethod(nickAccount)
} else if skelAccount != "" && nickAccount == "" {
@ -220,75 +232,47 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st
nickMethod := finalEnforcementMethod(nickAccount)
skelMethod := finalEnforcementMethod(skelAccount)
switch {
case skelMethod == NickReservationNone:
case skelMethod == NickEnforcementNone:
return nickAccount, nickMethod
case nickMethod == NickReservationNone:
case nickMethod == NickEnforcementNone:
return skelAccount, skelMethod
default:
// nobody can use this nick
return "!", NickReservationStrict
return "!", NickEnforcementStrict
}
}
}
func (am *AccountManager) BouncerAllowed(account string, session *Session) bool {
// TODO stub
config := am.server.Config()
if !config.Accounts.Bouncer.Enabled {
return false
}
if config.Accounts.Bouncer.AllowedByDefault {
return true
}
return session != nil && session.capabilities.Has(caps.Bouncer)
}
// 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) {
func (am *AccountManager) SetEnforcementStatus(account string, method NickEnforcementMethod) (finalSettings AccountSettings, err error) {
config := am.server.Config()
if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
return errFeatureDisabled
err = errFeatureDisabled
return
}
var serialized string
if method == NickReservationOptional {
serialized = "" // normally this is "default", but we're going to delete the key
} else {
serialized = nickReservationToString(method)
setter := func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.NickEnforcement = method
return out, nil
}
key := fmt.Sprintf(keyAccountEnforcement, account)
_, err = am.ModifyAccountSettings(account, setter)
if err != nil {
return
}
// this update of the data plane is racey, but it's probably fine
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
})
if method == NickEnforcementOptional {
delete(am.accountToMethod, account)
} else {
am.accountToMethod[account] = method
}
return nil
return
}
func (am *AccountManager) AccountToClients(account string) (result []*Client) {
@ -813,6 +797,12 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl
// pretend they have no vhost and move on
}
}
if raw.Settings != "" {
e := json.Unmarshal([]byte(raw.Settings), &result.Settings)
if e != nil {
am.server.logger.Warning("internal", "could not unmarshal settings for account", result.Name, e.Error())
}
}
return
}
@ -825,6 +815,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
_, e := tx.Get(accountKey)
if e == buntdb.ErrNotFound {
@ -838,6 +829,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
result.Callback, _ = tx.Get(callbackKey)
result.AdditionalNicks, _ = tx.Get(nicksKey)
result.VHost, _ = tx.Get(vhostKey)
result.Settings, _ = tx.Get(settingsKey)
if _, e = tx.Get(verifiedKey); e == nil {
result.Verified = true
@ -861,7 +853,7 @@ func (am *AccountManager) Unregister(account string) error {
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
enforcementKey := fmt.Sprintf(keyAccountEnforcement, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
@ -892,7 +884,7 @@ func (am *AccountManager) Unregister(account string) error {
tx.Delete(registeredTimeKey)
tx.Delete(callbackKey)
tx.Delete(verificationCodeKey)
tx.Delete(enforcementKey)
tx.Delete(settingsKey)
rawNicks, _ = tx.Get(nicksKey)
tx.Delete(nicksKey)
credText, err = tx.Get(credentialsKey)
@ -980,19 +972,13 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
}
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
err := am.server.store.View(func(tx *buntdb.Tx) error {
account, _ = tx.Get(certFPKey)
if account == "" {
return errAccountInvalidCredentials
}
rawAccount, err = am.loadRawAccount(tx, account)
if err != nil || !rawAccount.Verified {
return errAccountUnverified
}
return nil
})
@ -1001,14 +987,57 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
}
// ok, we found an account corresponding to their certificate
clientAccount, err := am.deserializeRawAccount(rawAccount)
clientAccount, err := am.LoadAccount(account)
if err != nil {
return err
} else if !clientAccount.Verified {
return errAccountUnverified
}
am.Login(client, clientAccount)
return nil
}
type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
func (am *AccountManager) ModifyAccountSettings(account string, munger settingsMunger) (newSettings AccountSettings, err error) {
casefoldedAccount, err := CasefoldName(account)
if err != nil {
return newSettings, errAccountDoesNotExist
}
// TODO implement this in general via a compare-and-swap API
accountData, err := am.LoadAccount(casefoldedAccount)
if err != nil {
return
} else if !accountData.Verified {
return newSettings, errAccountUnverified
}
newSettings, err = munger(accountData.Settings)
if err != nil {
return
}
text, err := json.Marshal(newSettings)
if err != nil {
return
}
key := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
serializedValue := string(text)
err = am.server.store.Update(func(tx *buntdb.Tx) (err error) {
_, _, err = tx.Set(key, serializedValue, nil)
return
})
if err != nil {
err = errAccountUpdateFailed
return
}
// success, push new settings into the client objects
am.Lock()
defer am.Unlock()
for _, client := range am.accountToClients[casefoldedAccount] {
client.SetAccountSettings(newSettings)
}
return
}
// represents someone's status in hostserv
type VHostInfo struct {
ApprovedVHost string
@ -1237,6 +1266,9 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) {
am.Lock()
defer am.Unlock()
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
for _, client := range am.accountToClients[casefoldedAccount] {
client.SetAccountSettings(account.Settings)
}
}
func (am *AccountManager) Logout(client *Client) {
@ -1283,6 +1315,21 @@ type AccountCredentials struct {
Certificate string // fingerprint
}
type BouncerAllowedSetting int
const (
BouncerAllowedServerDefault BouncerAllowedSetting = iota
BouncerDisallowedByUser
BouncerAllowedByUser
)
type AccountSettings struct {
AutoreplayLines *int
NickEnforcement NickEnforcementMethod
AllowBouncer BouncerAllowedSetting
AutoreplayJoins bool
}
// ClientAccount represents a user account.
type ClientAccount struct {
// Name of the account.
@ -1293,6 +1340,7 @@ type ClientAccount struct {
Verified bool
AdditionalNicks []string
VHost VHostInfo
Settings AccountSettings
}
// convenience for passing around raw serialized account data
@ -1304,6 +1352,7 @@ type rawClientAccount struct {
Verified bool
AdditionalNicks string
VHost string
Settings string
}
// logoutOfAccount logs the client out of their current account.

View File

@ -114,9 +114,3 @@ func (s *Set) String(version Version, values *Values) string {
return strings.Join(strs, " ")
}
// returns whether we should send `znc.in/self-message`-style echo messages
// to sessions other than that which originated the message
func (capabs *Set) SelfMessagesEnabled() bool {
return capabs.Has(EchoMessage) || capabs.Has(ZNCSelfMessage)
}

View File

@ -620,7 +620,17 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
rb.Flush(true)
replayLimit := channel.server.Config().History.AutoreplayOnJoin
var replayLimit int
customReplayLimit := client.AccountSettings().AutoreplayLines
if customReplayLimit != nil {
replayLimit = *customReplayLimit
maxLimit := channel.server.Config().History.ChathistoryMax
if maxLimit < replayLimit {
replayLimit = maxLimit
}
} else {
replayLimit = channel.server.Config().History.AutoreplayOnJoin
}
if 0 < replayLimit {
// TODO don't replay the client's own JOIN line?
items := channel.history.Latest(replayLimit)
@ -782,6 +792,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
client := rb.target
eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin)
playJoinsAsPrivmsg := (!autoreplay || client.AccountSettings().AutoreplayJoins)
if len(items) == 0 {
return
@ -808,7 +819,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
}
} else {
if autoreplay {
if !playJoinsAsPrivmsg {
continue // #474
}
var message string
@ -823,7 +834,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
} else {
if autoreplay {
if !playJoinsAsPrivmsg {
continue // #474
}
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message)
@ -840,7 +851,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
} else {
if autoreplay {
if !playJoinsAsPrivmsg {
continue // #474
}
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message)
@ -989,7 +1000,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
}
// send echo-message to other connected sessions
for _, session := range client.Sessions() {
if session == rb.session || !session.capabilities.SelfMessagesEnabled() {
if session == rb.session {
continue
}
var tagsToUse map[string]string
@ -998,7 +1009,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
}
if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) {
session.sendFromClientInternal(false, message.Time, message.Msgid, nickmask, account, tagsToUse, command, chname)
} else {
} else if histType != history.Tagmsg {
session.sendSplitMsgFromClientInternal(false, nickmask, account, tagsToUse, command, chname, message)
}
}

View File

@ -86,7 +86,7 @@ referenced by their registered account names, not their nicknames.`,
// csNotice sends the client a notice from ChanServ
func csNotice(rb *ResponseBuffer, text string) {
rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text)
rb.Add(nil, "ChanServ!ChanServ@localhost", "NOTICE", rb.target.Nick(), text)
}
func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {

View File

@ -48,6 +48,7 @@ type ResumeDetails struct {
type Client struct {
account string
accountName string // display name of the account: uncasefolded, '*' if not logged in
accountSettings AccountSettings
atime time.Time
away bool
awayMessage string
@ -600,7 +601,8 @@ func (client *Client) tryResumeChannels() {
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
var batchID string
nick := client.Nick()
details := client.Details()
nick := details.nick
if 0 < len(items) {
batchID = rb.StartNestedHistoryBatch(nick)
}
@ -626,7 +628,14 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
if allowTags {
tags = item.Tags
}
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
if item.Params[0] == "" {
// this message was sent *to* the client from another nick
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
} else {
// this message was sent *from* the client to another nick; the target is item.Params[0]
// substitute the client's current nickmask in case they changed nick
rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message)
}
}
rb.EndNestedBatch(batchID)

View File

@ -145,7 +145,20 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
account := client.Account()
bouncerAllowed := client.server.accounts.BouncerAllowed(account, session)
config := client.server.Config()
var bouncerAllowed bool
if config.Accounts.Bouncer.Enabled {
if session != nil && session.capabilities.Has(caps.Bouncer) {
bouncerAllowed = true
} else {
settings := client.AccountSettings()
if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
bouncerAllowed = true
} else if settings.AllowBouncer == BouncerAllowedByUser {
bouncerAllowed = true
}
}
}
clients.Lock()
defer clients.Unlock()
@ -168,7 +181,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
if skeletonHolder != nil && skeletonHolder != client {
return errNicknameInUse
}
if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account {
if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
return errNicknameReserved
}
clients.removeInternal(client)

View File

@ -112,63 +112,63 @@ type VHostConfig struct {
} `yaml:"user-requests"`
}
type NickReservationMethod int
type NickEnforcementMethod int
const (
// NickReservationOptional is the zero value; it serializes to
// NickEnforcementOptional 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
// defer to the default in the config. if both are NickEnforcementOptional then
// there is no enforcement.
NickReservationOptional NickReservationMethod = iota
NickReservationNone
NickReservationWithTimeout
NickReservationStrict
// XXX: these are serialized as numbers in the database, so beware of collisions
// when refactoring (any numbers currently in use must keep their meanings, or
// else be fixed up by a schema change)
NickEnforcementOptional NickEnforcementMethod = iota
NickEnforcementNone
NickEnforcementWithTimeout
NickEnforcementStrict
)
func nickReservationToString(method NickReservationMethod) string {
func nickReservationToString(method NickEnforcementMethod) string {
switch method {
case NickReservationOptional:
case NickEnforcementOptional:
return "default"
case NickReservationNone:
case NickEnforcementNone:
return "none"
case NickReservationWithTimeout:
case NickEnforcementWithTimeout:
return "timeout"
case NickReservationStrict:
case NickEnforcementStrict:
return "strict"
default:
return ""
}
}
func nickReservationFromString(method string) (NickReservationMethod, error) {
switch method {
func nickReservationFromString(method string) (NickEnforcementMethod, error) {
switch strings.ToLower(method) {
case "default":
return NickReservationOptional, nil
return NickEnforcementOptional, nil
case "optional":
return NickReservationOptional, nil
return NickEnforcementOptional, nil
case "none":
return NickReservationNone, nil
return NickEnforcementNone, nil
case "timeout":
return NickReservationWithTimeout, nil
return NickEnforcementWithTimeout, nil
case "strict":
return NickReservationStrict, nil
return NickEnforcementStrict, nil
default:
return NickReservationOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
return NickEnforcementOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
}
}
func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
var orig, raw string
func (nr *NickEnforcementMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
var orig string
var err error
if err = unmarshal(&orig); err != nil {
return err
}
if raw, err = Casefold(orig); err != nil {
return err
}
method, err := nickReservationFromString(raw)
method, err := nickReservationFromString(orig)
if err == nil {
*nr = method
}
@ -178,7 +178,7 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
type NickReservationConfig struct {
Enabled bool
AdditionalNickLimit int `yaml:"additional-nick-limit"`
Method NickReservationMethod
Method NickEnforcementMethod
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
RenameTimeout time.Duration `yaml:"rename-timeout"`
RenamePrefix string `yaml:"rename-prefix"`

View File

@ -22,7 +22,7 @@ const (
// 'version' of the database schema
keySchemaVersion = "db.version"
// latest schema of the db
latestDbSchema = "5"
latestDbSchema = "6"
)
type SchemaChanger func(*Config, *buntdb.Tx) error
@ -409,6 +409,37 @@ func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error {
return nil
}
// custom nick enforcement was a separate db key, now it's part of settings
func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error {
accountToEnforcement := make(map[string]NickEnforcementMethod)
prefix := "account.customenforcement "
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
account := strings.TrimPrefix(key, prefix)
method, err := nickReservationFromString(value)
if err == nil {
accountToEnforcement[account] = method
} else {
log.Printf("skipping corrupt custom enforcement value for %s\n", account)
}
return true
})
for account, method := range accountToEnforcement {
var settings AccountSettings
settings.NickEnforcement = method
text, err := json.Marshal(settings)
if err != nil {
return err
}
tx.Delete(prefix + account)
tx.Set(fmt.Sprintf("account.settings %s", account), string(text), nil)
}
return nil
}
func init() {
allChanges := []SchemaChange{
{
@ -431,6 +462,11 @@ func init() {
TargetVersion: "5",
Changer: schemaChangeV4ToV5,
},
{
InitialVersion: "5",
TargetVersion: "6",
Changer: schemaChangeV5ToV6,
},
}
// build the index

View File

@ -5,7 +5,10 @@
package irc
import "errors"
import (
"errors"
"github.com/oragono/oragono/irc/utils"
)
// Runtime Errors
var (
@ -19,10 +22,10 @@ var (
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
errAccountNotLoggedIn = errors.New("You're not logged into an account")
errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
errAccountUnverified = errors.New("Account is not yet verified")
errAccountUnverified = errors.New(`Account is not yet verified`)
errAccountVerificationFailed = errors.New("Account verification failed")
errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
errAccountUpdateFailed = errors.New("Error while updating your account information")
errAccountUpdateFailed = errors.New(`Error while updating your account information`)
errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`)
errCallbackFailed = errors.New("Account verification could not be sent")
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
@ -40,7 +43,7 @@ var (
errInvalidUsername = errors.New("Invalid username")
errFeatureDisabled = errors.New(`That feature is disabled`)
errBanned = errors.New("IP or nickmask banned")
errInvalidParams = errors.New("Invalid parameters")
errInvalidParams = utils.ErrInvalidParams
)
// Socket Errors

View File

@ -275,6 +275,19 @@ func (client *Client) SetAccountName(account string) (changed bool) {
return
}
func (client *Client) AccountSettings() (result AccountSettings) {
client.stateMutex.RLock()
result = client.accountSettings
client.stateMutex.RUnlock()
return
}
func (client *Client) SetAccountSettings(settings AccountSettings) {
client.stateMutex.Lock()
client.accountSettings = settings
client.stateMutex.Unlock()
}
func (client *Client) Languages() (languages []string) {
client.stateMutex.RLock()
languages = client.languages

View File

@ -2046,12 +2046,12 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
}
// an echo-message may need to go out to other client sessions:
for _, session := range client.Sessions() {
if session == rb.session || !rb.session.capabilities.SelfMessagesEnabled() {
if session == rb.session {
continue
}
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
} else {
} else if histType != history.Tagmsg {
session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
}
}
@ -2060,12 +2060,17 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, user.AwayMessage())
}
user.history.Add(history.Item{
item := history.Item{
Type: histType,
Message: splitMsg,
Nick: nickMaskString,
AccountName: accountName,
})
}
// add to the target's history:
user.history.Add(item)
// add this to the client's history as well, recording the target:
item.Params[0] = tnick
client.history.Add(item)
}
}
return false

View File

@ -131,7 +131,7 @@ for the rejection.`,
// hsNotice sends the client a notice from HostServ
func hsNotice(rb *ResponseBuffer, text string) {
rb.Add(nil, "HostServ", "NOTICE", rb.target.Nick(), text)
rb.Add(nil, "HostServ!HostServ@localhost", "NOTICE", rb.target.Nick(), text)
}
// hsNotifyChannel notifies the designated channel of new vhost activity

View File

@ -198,7 +198,7 @@ func (nt *NickTimer) Initialize(client *Client) {
}
config := &client.server.Config().Accounts.NickReservation
enabled := config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)
enabled := config.Enabled && (config.Method == NickEnforcementWithTimeout || config.AllowCustomEnforcement)
nt.Lock()
defer nt.Unlock()
@ -235,7 +235,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) {
cfnick, skeleton := nt.client.uniqueIdentifiers()
account := nt.client.Account()
accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton)
enforceTimeout := method == NickReservationWithTimeout
enforceTimeout := method == NickEnforcementWithTimeout
var shouldWarn, shouldRename bool
@ -258,7 +258,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) {
if enforceTimeout && delinquent && (accountChanged || nt.timer == nil) {
nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
shouldWarn = true
} else if method == NickReservationStrict && delinquent {
} else if method == NickEnforcementStrict && delinquent {
shouldRename = true // this can happen if reservation was enabled by rehash
}
}()

View File

@ -5,11 +5,14 @@ package irc
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/modes"
"github.com/oragono/oragono/irc/utils"
)
// "enabled" callbacks for specific nickserv commands
@ -25,10 +28,6 @@ func servCmdRequiresNickRes(config *Config) bool {
return config.Accounts.AuthenticationEnabled && config.Accounts.NickReservation.Enabled
}
func nsEnforceEnabled(config *Config) bool {
return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement
}
func servCmdRequiresBouncerEnabled(config *Config) bool {
return config.Accounts.Bouncer.Enabled
}
@ -61,20 +60,14 @@ DROP de-links the given (or your current) nickname from your user account.`,
authRequired: true,
},
"enforce": {
hidden: true,
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.`,
ENFORCE is an alias for $bGET enforce$b and $bSET enforce$b. See the help
entry for $bSET$b for more information.`,
authRequired: true,
enabled: nsEnforceEnabled,
enabled: servCmdRequiresAccreg,
},
"ghost": {
handler: nsGhostHandler,
@ -194,12 +187,246 @@ password by supplying their username and then the desired password.`,
enabled: servCmdRequiresAuthEnabled,
minParams: 2,
},
"get": {
handler: nsGetHandler,
help: `Syntax: $bGET <setting>$b
GET queries the current values of your account settings. For more information
on the settings and their possible values, see HELP SET.`,
helpShort: `$bGET$b queries the current values of your account settings`,
authRequired: true,
enabled: servCmdRequiresAccreg,
minParams: 1,
},
"saget": {
handler: nsGetHandler,
help: `Syntax: $bSAGET <account> <setting>$b
SAGET queries the values of someone else's account settings. For more
information on the settings and their possible values, see HELP SET.`,
helpShort: `$bSAGET$b queries the current values of another user's account settings`,
enabled: servCmdRequiresAccreg,
minParams: 2,
capabs: []string{"accreg"},
},
"set": {
handler: nsSetHandler,
helpShort: `$bSET$b modifies your account settings`,
// these are broken out as separate strings so they can be translated separately
helpStrings: []string{
`Syntax $bSET <setting> <value>$b
Set modifies your account settings. The following settings are available:`,
`$bENFORCE$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]`,
`$bBOUNCER$b
If 'bouncer' is enabled and you are already logged in and using a nick, a
second client of yours that authenticates with SASL and requests the same nick
is allowed to attach to the nick as well (this is comparable to the behavior
of IRC "bouncers" like ZNC). Your options are 'on' (allow this behavior),
'off' (disallow it), and 'default' (use the server default value).`,
`$bAUTOREPLAY-LINES$b
'autoreplay-lines' controls the number of lines of channel history that will
be replayed to you automatically when joining a channel. Your options are any
positive number, 0 to disable the feature, and 'default' to use the server
default.`,
`$bAUTOREPLAY-JOINS$b
'autoreplay-joins' controls whether autoreplayed channel history will include
lines for join and part. This provides more information about the context of
messages, but may be spammy. Your options are 'on' and 'off'.`,
},
authRequired: true,
enabled: servCmdRequiresAccreg,
minParams: 2,
},
"saset": {
handler: nsSetHandler,
help: `Syntax: $bSASET <account> <setting> <value>$b
SASET modifies the values of someone else's account settings. For more
information on the settings and their possible values, see HELP SET.`,
helpShort: `$bSASET$b modifies another user's account settings`,
enabled: servCmdRequiresAccreg,
minParams: 3,
capabs: []string{"accreg"},
},
}
)
// nsNotice sends the client a notice from NickServ
func nsNotice(rb *ResponseBuffer, text string) {
rb.Add(nil, "NickServ", "NOTICE", rb.target.Nick(), text)
// XXX i can't figure out how to use OragonoServices[servicename].prefix here
// without creating a compile-time initialization loop
rb.Add(nil, "NickServ!NickServ@localhost", "NOTICE", rb.target.Nick(), text)
}
func nsGetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var account string
if command == "saget" {
account = params[0]
params = params[1:]
} else {
account = client.Account()
}
accountData, err := server.accounts.LoadAccount(account)
if err == errAccountDoesNotExist {
nsNotice(rb, client.t("No such account"))
return
} else if err != nil {
nsNotice(rb, client.t("Error loading account data"))
return
}
displaySetting(params[0], accountData.Settings, client, rb)
}
func displaySetting(settingName string, settings AccountSettings, client *Client, rb *ResponseBuffer) {
config := client.server.Config()
switch strings.ToLower(settingName) {
case "enforce":
storedValue := settings.NickEnforcement
serializedStoredValue := nickReservationToString(storedValue)
nsNotice(rb, fmt.Sprintf(client.t("Your stored nickname enforcement setting is: %s"), serializedStoredValue))
serializedActualValue := nickReservationToString(configuredEnforcementMethod(config, storedValue))
nsNotice(rb, fmt.Sprintf(client.t("Given current server settings, your nickname is enforced with: %s"), serializedActualValue))
case "autoreplay-lines":
if settings.AutoreplayLines == nil {
nsNotice(rb, fmt.Sprintf(client.t("You will receive the server default of %d lines of autoreplayed history"), config.History.AutoreplayOnJoin))
} else {
nsNotice(rb, fmt.Sprintf(client.t("You will receive %d lines of autoreplayed history"), *settings.AutoreplayLines))
}
case "autoreplay-joins":
if settings.AutoreplayJoins {
nsNotice(rb, client.t("You will see JOINs and PARTs in autoreplayed history lines"))
} else {
nsNotice(rb, client.t("You will not see JOINs and PARTs in autoreplayed history lines"))
}
case "bouncer":
if !config.Accounts.Bouncer.Enabled {
nsNotice(rb, fmt.Sprintf(client.t("This feature has been disabled by the server administrators")))
} else {
switch settings.AllowBouncer {
case BouncerAllowedServerDefault:
if config.Accounts.Bouncer.AllowedByDefault {
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account, but you can opt out")))
} else {
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account, but you can opt in")))
}
case BouncerDisallowedByUser:
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account")))
case BouncerAllowedByUser:
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account")))
}
}
default:
nsNotice(rb, client.t("No such setting"))
}
}
func nsSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var account string
if command == "saset" {
account = params[0]
params = params[1:]
} else {
account = client.Account()
}
var munger settingsMunger
var finalSettings AccountSettings
var err error
switch strings.ToLower(params[0]) {
case "pass":
nsNotice(rb, client.t("To change a password, use the PASSWD command. For details, /msg NickServ HELP PASSWD"))
return
case "enforce":
var method NickEnforcementMethod
method, err = nickReservationFromString(params[1])
if err != nil {
err = errInvalidParams
break
}
// updating enforcement settings is special-cased, because it requires
// an update to server.accounts.accountToMethod
finalSettings, err = server.accounts.SetEnforcementStatus(account, method)
if err == nil {
finalSettings.NickEnforcement = method // success
}
case "autoreplay-lines":
var newValue *int
if strings.ToLower(params[1]) != "default" {
val, err_ := strconv.Atoi(params[1])
if err_ != nil || val < 0 {
err = errInvalidParams
break
}
newValue = new(int)
*newValue = val
}
munger = func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.AutoreplayLines = newValue
return
}
case "bouncer":
var newValue BouncerAllowedSetting
if strings.ToLower(params[1]) == "default" {
newValue = BouncerAllowedServerDefault
} else {
var enabled bool
enabled, err = utils.StringToBool(params[1])
if enabled {
newValue = BouncerAllowedByUser
} else {
newValue = BouncerDisallowedByUser
}
}
if err == nil {
munger = func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.AllowBouncer = newValue
return
}
}
case "autoreplay-joins":
var newValue bool
newValue, err = utils.StringToBool(params[1])
if err == nil {
munger = func(in AccountSettings) (out AccountSettings, err error) {
out = in
out.AutoreplayJoins = newValue
return
}
}
default:
err = errInvalidParams
}
if munger != nil {
finalSettings, err = server.accounts.ModifyAccountSettings(account, munger)
}
switch err {
case nil:
nsNotice(rb, client.t("Successfully changed your account settings"))
displaySetting(params[0], finalSettings, client, rb)
case errInvalidParams, errAccountDoesNotExist, errFeatureDisabled, errAccountUnverified, errAccountUpdateFailed:
nsNotice(rb, client.t(err.Error()))
default:
// unknown error
nsNotice(rb, client.t("An error occurred"))
}
}
func nsDropHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
@ -550,7 +777,7 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
}
}
default:
errorMessage = "Invalid parameters"
errorMessage = `Invalid parameters`
}
if errorMessage != "" {
@ -568,22 +795,12 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
}
func nsEnforceHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
newParams := []string{"enforce"}
if len(params) == 0 {
status := server.accounts.getStoredEnforcementStatus(client.Account())
nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status))
nsGetHandler(server, client, "get", newParams, rb)
} else {
method, err := nickReservationFromString(params[0])
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"))
}
newParams = append(newParams, params[0])
nsSetHandler(server, client, "set", newParams, rb)
}
}

View File

@ -18,7 +18,7 @@ type resumeTokenPair struct {
}
type ResumeManager struct {
sync.RWMutex // level 2
sync.Mutex // level 2
resumeIDtoCreds map[string]resumeTokenPair
server *Server
@ -59,8 +59,8 @@ func (rm *ResumeManager) VerifyToken(token string) (client *Client) {
return
}
rm.RLock()
defer rm.RUnlock()
rm.Lock()
defer rm.Unlock()
id := token[:utils.SecretTokenLength]
pair, ok := rm.resumeIDtoCreds[id]

View File

@ -18,6 +18,7 @@ import (
type ircService struct {
Name string
ShortName string
prefix string
CommandAliases []string
Commands map[string]*serviceCommand
HelpBanner string
@ -29,8 +30,10 @@ type serviceCommand struct {
capabs []string // oper capabs the given user has to have to access this command
handler func(server *Server, client *Client, command string, params []string, rb *ResponseBuffer)
help string
helpStrings []string
helpShort string
authRequired bool
hidden bool
enabled func(*Config) bool // is this command enabled in the server config?
minParams int
maxParams int // split into at most n params, with last param containing remaining unsplit text
@ -139,7 +142,7 @@ func servicePrivmsgHandler(service *ircService, server *Server, client *Client,
func serviceRunCommand(service *ircService, server *Server, client *Client, cmd *serviceCommand, commandName string, params []string, rb *ResponseBuffer) {
nick := rb.target.Nick()
sendNotice := func(notice string) {
rb.Add(nil, service.Name, "NOTICE", nick, notice)
rb.Add(nil, service.prefix, "NOTICE", nick, notice)
}
if cmd == nil {
@ -180,7 +183,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
nick := rb.target.Nick()
config := server.Config()
sendNotice := func(notice string) {
rb.Add(nil, service.Name, "NOTICE", nick, notice)
rb.Add(nil, service.prefix, "NOTICE", nick, notice)
}
sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
@ -194,7 +197,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
continue
}
if commandInfo.aliasOf != "" {
if commandInfo.aliasOf != "" || commandInfo.hidden {
continue // don't show help lines for aliases
}
if commandInfo.enabled != nil && !commandInfo.enabled(config) {
@ -226,8 +229,18 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
if commandInfo == nil {
sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName)))
} else {
for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
sendNotice(line)
helpStrings := commandInfo.helpStrings
if helpStrings == nil {
hsArray := [1]string{commandInfo.help}
helpStrings = hsArray[:]
}
for i, helpString := range helpStrings {
if 0 < i {
sendNotice("")
}
for _, line := range strings.Split(ircfmt.Unescape(client.t(helpString)), "\n") {
sendNotice(line)
}
}
}
}
@ -241,6 +254,8 @@ func initializeServices() {
oragonoServicesByCommandAlias = make(map[string]*ircService)
for serviceName, service := range OragonoServices {
service.prefix = fmt.Sprintf("%s!%s@localhost", service.Name, service.Name)
// make `/MSG ServiceName HELP` work correctly
service.Commands["help"] = &servHelpCmd
@ -257,8 +272,10 @@ func initializeServices() {
// force devs to write a help entry for every command
for commandName, commandInfo := range service.Commands {
if commandInfo.aliasOf == "" && (commandInfo.help == "" || commandInfo.helpShort == "") {
log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
if commandInfo.aliasOf == "" && !commandInfo.hidden {
if (commandInfo.help == "" && commandInfo.helpStrings == nil) || commandInfo.helpShort == "" {
log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
}
}
}
}

View File

@ -3,6 +3,15 @@
package utils
import (
"errors"
"strings"
)
var (
ErrInvalidParams = errors.New("Invalid parameters")
)
// ArgsToStrings takes the arguments and splits them into a series of strings,
// each argument separated by delim and each string bounded by maxLength.
func ArgsToStrings(maxLength int, arguments []string, delim string) []string {
@ -33,3 +42,15 @@ func ArgsToStrings(maxLength int, arguments []string, delim string) []string {
return messages
}
func StringToBool(str string) (result bool, err error) {
switch strings.ToLower(str) {
case "on", "true", "t", "yes", "y":
result = true
case "off", "false", "f", "no", "n":
result = false
default:
err = ErrInvalidParams
}
return
}

23
irc/utils/args_test.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package utils
import "testing"
func TestStringToBool(t *testing.T) {
val, err := StringToBool("on")
assertEqual(val, true, t)
assertEqual(err, nil, t)
val, err = StringToBool("n")
assertEqual(val, false, t)
assertEqual(err, nil, t)
val, err = StringToBool("OFF")
assertEqual(val, false, t)
assertEqual(err, nil, t)
val, err = StringToBool("default")
assertEqual(err, ErrInvalidParams, t)
}

View File

@ -134,7 +134,7 @@ server:
# defaults to true when unset for that reason.
force-trailing: true
# some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower, Adium) do not
# some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower) do not
# respond correctly to SASL messages with the server name as a prefix:
# https://github.com/znc/znc/issues/1212
# this works around that bug, allowing them to use SASL.