Merge pull request #1347 from slingamn/issue1346.1

fix #1346
This commit is contained in:
Shivaram Lingamneni 2020-10-22 21:16:02 -07:00 committed by GitHub
commit c2c5fe7cf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 35 additions and 393 deletions

View File

@ -475,23 +475,6 @@ accounts:
# (make sure any changes you make here are RFC-compliant)
valid-regexp: '^[0-9A-Za-z.\-_/]+$'
# options controlling users requesting vhosts:
user-requests:
# can users request vhosts at all? if this is false, operators with the
# 'vhosts' capability can still assign vhosts manually
enabled: false
# if uncommented, all new vhost requests will be dumped into the given
# channel, so opers can review them as they are sent in. ensure that you
# have registered and restricted the channel appropriately before you
# uncomment this.
#channel: "#vhosts"
# after a user's vhost has been approved or rejected, they need to wait
# this long (starting from the time of their original request)
# before they can request a new one.
cooldown: 168h
# modes that are set by default when a user connects
# if unset, no user modes will be set by default
# +i is invisible (a user's channels are hidden from whois replies)

View File

@ -503,23 +503,6 @@ accounts:
# (make sure any changes you make here are RFC-compliant)
valid-regexp: '^[0-9A-Za-z.\-_/]+$'
# options controlling users requesting vhosts:
user-requests:
# can users request vhosts at all? if this is false, operators with the
# 'vhosts' capability can still assign vhosts manually
enabled: false
# if uncommented, all new vhost requests will be dumped into the given
# channel, so opers can review them as they are sent in. ensure that you
# have registered and restricted the channel appropriately before you
# uncomment this.
#channel: "#vhosts"
# after a user's vhost has been approved or rejected, they need to wait
# this long (starting from the time of their original request)
# before they can request a new one.
cooldown: 168h
# modes that are set by default when a user connects
# if unset, no user modes will be set by default
# +i is invisible (a user's channels are hidden from whois replies)

View File

@ -12,7 +12,6 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"unicode"
@ -44,23 +43,14 @@ const (
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
keyAccountRealname = "account.realname %s" // client realname stored as string
keyVHostQueueAcctToId = "vhostQueue %s"
vhostRequestIdx = "vhostQueue"
maxCertfpsPerAccount = 5
)
// everything about accounts is persistent; therefore, the database is the authoritative
// source of truth for all account information. anything on the heap is just a cache
type AccountManager struct {
// XXX these are up here so they can be aligned to a 64-bit boundary, please forgive me
// autoincrementing ID for vhost requests:
vhostRequestID uint64
vhostRequestPendingCount uint64
sync.RWMutex // tier 2
serialCacheUpdateMutex sync.Mutex // tier 3
vHostUpdateMutex sync.Mutex // tier 3
server *Server
// track clients logged in to accounts
@ -80,7 +70,6 @@ func (am *AccountManager) Initialize(server *Server) {
config := server.Config()
am.buildNickToAccountIndex(config)
am.initVHostRequestQueue(config)
am.createAlwaysOnClients(config)
am.resetRegisterThrottle(config)
}
@ -225,44 +214,6 @@ func (am *AccountManager) buildNickToAccountIndex(config *Config) {
}
}
func (am *AccountManager) initVHostRequestQueue(config *Config) {
if !config.Accounts.VHosts.Enabled {
return
}
am.vHostUpdateMutex.Lock()
defer am.vHostUpdateMutex.Unlock()
// the db maps the account name to the autoincrementing integer ID of its request
// create an numerically ordered index on ID, so we can list the oldest requests
// finally, collect the integer id of the newest request and the total request count
var total uint64
var lastIDStr string
err := am.server.store.Update(func(tx *buntdb.Tx) error {
err := tx.CreateIndex(vhostRequestIdx, fmt.Sprintf(keyVHostQueueAcctToId, "*"), buntdb.IndexInt)
if err != nil {
return err
}
return tx.Descend(vhostRequestIdx, func(key, value string) bool {
if lastIDStr == "" {
lastIDStr = value
}
total++
return true
})
})
if err != nil {
am.server.logger.Error("internal", "could not create vhost queue index", err.Error())
}
lastID, _ := strconv.ParseUint(lastIDStr, 10, 64)
am.server.logger.Debug("services", fmt.Sprintf("vhost queue length is %d, autoincrementing id is %d", total, lastID))
atomic.StoreUint64(&am.vhostRequestID, lastID)
atomic.StoreUint64(&am.vhostRequestPendingCount, total)
}
func (am *AccountManager) NickToAccount(nick string) string {
cfnick, err := CasefoldName(nick)
if err != nil {
@ -1394,7 +1345,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount)
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
@ -1461,8 +1411,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
tx.Delete(modesKey)
tx.Delete(realnameKey)
_, err := tx.Delete(vhostQueueKey)
am.decrementVHostQueueCount(casefoldedAccount, err)
return nil
})
@ -1637,40 +1585,6 @@ func (am *AccountManager) ModifyAccountSettings(account string, munger settingsM
type VHostInfo struct {
ApprovedVHost string
Enabled bool
RequestedVHost string
RejectedVHost string
RejectionReason string
LastRequestTime time.Time
}
// pair type, <VHostInfo, accountName>
type PendingVHostRequest struct {
VHostInfo
Account string
}
type vhostThrottleExceeded struct {
timeRemaining time.Duration
}
func (vhe *vhostThrottleExceeded) Error() string {
return fmt.Sprintf("Wait at least %v and try again", vhe.timeRemaining)
}
func (vh *VHostInfo) checkThrottle(cooldown time.Duration) (err error) {
if cooldown == 0 {
return nil
}
now := time.Now().UTC()
elapsed := now.Sub(vh.LastRequestTime)
if elapsed > cooldown {
// success
vh.LastRequestTime = now
return nil
} else {
return &vhostThrottleExceeded{timeRemaining: cooldown - elapsed}
}
}
// callback type implementing the actual business logic of vhost operations
@ -1687,52 +1601,6 @@ func (am *AccountManager) VHostSet(account string, vhost string) (result VHostIn
return am.performVHostChange(account, munger)
}
func (am *AccountManager) VHostRequest(account string, vhost string, cooldown time.Duration) (result VHostInfo, err error) {
munger := func(input VHostInfo) (output VHostInfo, err error) {
output = input
// you can update your existing request, but if you were approved or rejected,
// you can't spam a new request
if output.RequestedVHost == "" {
err = output.checkThrottle(cooldown)
}
if err != nil {
return
}
output.RequestedVHost = vhost
output.RejectedVHost = ""
output.RejectionReason = ""
output.LastRequestTime = time.Now().UTC()
return
}
return am.performVHostChange(account, munger)
}
func (am *AccountManager) VHostApprove(account string) (result VHostInfo, err error) {
munger := func(input VHostInfo) (output VHostInfo, err error) {
output = input
output.Enabled = true
output.ApprovedVHost = input.RequestedVHost
output.RequestedVHost = ""
output.RejectionReason = ""
return
}
return am.performVHostChange(account, munger)
}
func (am *AccountManager) VHostReject(account string, reason string) (result VHostInfo, err error) {
munger := func(input VHostInfo) (output VHostInfo, err error) {
output = input
output.RejectedVHost = output.RequestedVHost
output.RequestedVHost = ""
output.RejectionReason = reason
return
}
return am.performVHostChange(account, munger)
}
func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result VHostInfo, err error) {
munger := func(input VHostInfo) (output VHostInfo, err error) {
if input.ApprovedVHost == "" {
@ -1759,9 +1627,6 @@ func (am *AccountManager) performVHostChange(account string, munger vhostMunger)
return
}
am.vHostUpdateMutex.Lock()
defer am.vHostUpdateMutex.Unlock()
clientAccount, err := am.LoadAccount(account)
if err != nil {
err = errAccountDoesNotExist
@ -1784,25 +1649,9 @@ func (am *AccountManager) performVHostChange(account string, munger vhostMunger)
vhstr := string(vhtext)
key := fmt.Sprintf(keyAccountVHost, account)
queueKey := fmt.Sprintf(keyVHostQueueAcctToId, account)
err = am.server.store.Update(func(tx *buntdb.Tx) error {
if _, _, err := tx.Set(key, vhstr, nil); err != nil {
_, _, err := tx.Set(key, vhstr, nil)
return err
}
// update request queue
if clientAccount.VHost.RequestedVHost == "" && result.RequestedVHost != "" {
id := atomic.AddUint64(&am.vhostRequestID, 1)
if _, _, err = tx.Set(queueKey, strconv.FormatUint(id, 10), nil); err != nil {
return err
}
atomic.AddUint64(&am.vhostRequestPendingCount, 1)
} else if clientAccount.VHost.RequestedVHost != "" && result.RequestedVHost == "" {
_, err = tx.Delete(queueKey)
am.decrementVHostQueueCount(account, err)
}
return nil
})
if err != nil {
@ -1814,51 +1663,6 @@ func (am *AccountManager) performVHostChange(account string, munger vhostMunger)
return result, nil
}
// XXX annoying helper method for keeping the queue count in sync with the DB
// `err` is the buntdb error returned from deleting the queue key
func (am *AccountManager) decrementVHostQueueCount(account string, err error) {
if err == nil {
// successfully deleted a queue entry, do a 2's complement decrement:
atomic.AddUint64(&am.vhostRequestPendingCount, ^uint64(0))
} else if err != buntdb.ErrNotFound {
am.server.logger.Error("internal", "buntdb dequeue error", account, err.Error())
}
}
func (am *AccountManager) VHostListRequests(limit int) (requests []PendingVHostRequest, total int) {
am.vHostUpdateMutex.Lock()
defer am.vHostUpdateMutex.Unlock()
total = int(atomic.LoadUint64(&am.vhostRequestPendingCount))
prefix := fmt.Sprintf(keyVHostQueueAcctToId, "")
accounts := make([]string, 0, limit)
err := am.server.store.View(func(tx *buntdb.Tx) error {
return tx.Ascend(vhostRequestIdx, func(key, value string) bool {
accounts = append(accounts, strings.TrimPrefix(key, prefix))
return len(accounts) < limit
})
})
if err != nil {
am.server.logger.Error("internal", "couldn't traverse vhost queue", err.Error())
return
}
for _, account := range accounts {
accountInfo, err := am.LoadAccount(account)
if err == nil {
requests = append(requests, PendingVHostRequest{
Account: account,
VHostInfo: accountInfo.VHost,
})
} else {
am.server.logger.Error("internal", "corrupt account", account, err.Error())
}
}
return
}
func (am *AccountManager) applyVHostInfo(client *Client, info VHostInfo) {
// if hostserv is disabled in config, then don't grant vhosts
// that were previously approved while it was enabled

View File

@ -316,12 +316,7 @@ type VHostConfig struct {
Enabled bool
MaxLength int `yaml:"max-length"`
ValidRegexpRaw string `yaml:"valid-regexp"`
ValidRegexp *regexp.Regexp
UserRequests struct {
Enabled bool
Channel string
Cooldown custime.Duration
} `yaml:"user-requests"`
validRegexp *regexp.Regexp
}
type NickEnforcementMethod int
@ -1109,13 +1104,13 @@ func LoadConfig(filename string) (config *Config, err error) {
if rawRegexp != "" {
regexp, err := regexp.Compile(rawRegexp)
if err == nil {
config.Accounts.VHosts.ValidRegexp = regexp
config.Accounts.VHosts.validRegexp = regexp
} else {
log.Printf("invalid vhost regexp: %s\n", err.Error())
}
}
if config.Accounts.VHosts.ValidRegexp == nil {
config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
if config.Accounts.VHosts.validRegexp == nil {
config.Accounts.VHosts.validRegexp = defaultValidVhostRegex
}
config.Server.capValues[caps.SASL] = "PLAIN,EXTERNAL"

View File

@ -24,7 +24,7 @@ const (
// 'version' of the database schema
keySchemaVersion = "db.version"
// latest schema of the db
latestDbSchema = "16"
latestDbSchema = "17"
keyCloakSecret = "crypto.cloak_secret"
)
@ -835,6 +835,24 @@ func schemaChangeV15ToV16(config *Config, tx *buntdb.Tx) error {
return nil
}
// #1346: remove vhost request queue
func schemaChangeV16ToV17(config *Config, tx *buntdb.Tx) error {
prefix := "vhostQueue "
var keys []string
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
keys = append(keys, key)
return true
})
for _, key := range keys {
tx.Delete(key)
}
return nil
}
func init() {
allChanges := []SchemaChange{
{
@ -912,6 +930,11 @@ func init() {
TargetVersion: "16",
Changer: schemaChangeV15ToV16,
},
{
InitialVersion: "16",
TargetVersion: "17",
Changer: schemaChangeV16ToV17,
},
}
// build the index

View File

@ -7,11 +7,9 @@ import (
"errors"
"fmt"
"regexp"
"time"
"github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/sno"
"github.com/oragono/oragono/irc/utils"
)
@ -32,10 +30,6 @@ func hostservEnabled(config *Config) bool {
return config.Accounts.VHosts.Enabled
}
func hostservRequestsEnabled(config *Config) bool {
return config.Accounts.VHosts.Enabled && config.Accounts.VHosts.UserRequests.Enabled
}
var (
hostservCommands = map[string]*serviceCommand{
"on": {
@ -56,17 +50,6 @@ OFF disables your vhost, if you have one approved.`,
authRequired: true,
enabled: hostservEnabled,
},
"request": {
handler: hsRequestHandler,
help: `Syntax: $bREQUEST <vhost>$b
REQUEST requests that a new vhost by assigned to your account. The request must
then be approved by a server operator.`,
helpShort: `$bREQUEST$b requests a new vhost, pending operator approval.`,
authRequired: true,
enabled: hostservRequestsEnabled,
minParams: 1,
},
"status": {
handler: hsStatusHandler,
help: `Syntax: $bSTATUS [user]$b
@ -96,39 +79,6 @@ DEL deletes a user's vhost.`,
enabled: hostservEnabled,
minParams: 1,
},
"waiting": {
handler: hsWaitingHandler,
help: `Syntax: $bWAITING$b
WAITING shows a list of pending vhost requests, which can then be approved
or rejected.`,
helpShort: `$bWAITING$b shows a list of pending vhost requests.`,
capabs: []string{"vhosts"},
enabled: hostservEnabled,
},
"approve": {
handler: hsApproveHandler,
help: `Syntax: $bAPPROVE <user>$b
APPROVE approves a user's vhost request.`,
helpShort: `$bAPPROVE$b approves a user's vhost request.`,
capabs: []string{"vhosts"},
enabled: hostservEnabled,
minParams: 1,
},
"reject": {
handler: hsRejectHandler,
help: `Syntax: $bREJECT <user> [<reason>]$b
REJECT rejects a user's vhost request, optionally giving them a reason
for the rejection.`,
helpShort: `$bREJECT$b rejects a user's vhost request.`,
capabs: []string{"vhosts"},
enabled: hostservEnabled,
minParams: 1,
maxParams: 2,
unsplitFinalParam: true,
},
"setcloaksecret": {
handler: hsSetCloakSecretHandler,
help: `Syntax: $bSETCLOAKSECRET$b <secret> [code]
@ -150,19 +100,6 @@ func hsNotice(rb *ResponseBuffer, text string) {
rb.Add(nil, hsNickMask, "NOTICE", rb.target.Nick(), text)
}
// hsNotifyChannel notifies the designated channel of new vhost activity
func hsNotifyChannel(server *Server, message string) {
chname := server.Config().Accounts.VHosts.UserRequests.Channel
channel := server.channels.Get(chname)
if channel == nil {
return
}
chname = channel.Name()
for _, client := range channel.Members() {
client.Send(nil, hsNickMask, "PRIVMSG", chname, message)
}
}
func hsOnOffHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
enable := false
if command == "on" {
@ -181,29 +118,6 @@ func hsOnOffHandler(server *Server, client *Client, command string, params []str
}
}
func hsRequestHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
vhost := params[0]
if validateVhost(server, vhost, false) != nil {
hsNotice(rb, client.t("Invalid vhost"))
return
}
accountName := client.Account()
_, err := server.accounts.VHostRequest(accountName, vhost, time.Duration(server.Config().Accounts.VHosts.UserRequests.Cooldown))
if err != nil {
if throttled, ok := err.(*vhostThrottleExceeded); ok {
hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), throttled.timeRemaining))
} else {
hsNotice(rb, client.t("An error occurred"))
}
} else {
hsNotice(rb, client.t("Your vhost request will be reviewed by an administrator"))
chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost)
hsNotifyChannel(server, chanMsg)
server.snomasks.Send(sno.LocalVhosts, chanMsg)
}
}
func hsStatusHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var accountName string
if len(params) > 0 {
@ -237,13 +151,6 @@ func hsStatusHandler(server *Server, client *Client, command string, params []st
} else {
hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName))
}
if account.VHost.RequestedVHost != "" {
hsNotice(rb, fmt.Sprintf(client.t("A request is pending for vhost: %s"), account.VHost.RequestedVHost))
}
if account.VHost.RejectedVHost != "" {
hsNotice(rb, fmt.Sprintf(client.t("A request was previously made for vhost: %s"), account.VHost.RejectedVHost))
hsNotice(rb, fmt.Sprintf(client.t("It was rejected for reason: %s"), account.VHost.RejectionReason))
}
}
func validateVhost(server *Server, vhost string, oper bool) error {
@ -251,7 +158,7 @@ func validateVhost(server *Server, vhost string, oper bool) error {
if len(vhost) > config.Accounts.VHosts.MaxLength {
return errVHostTooLong
}
if !config.Accounts.VHosts.ValidRegexp.MatchString(vhost) {
if !config.Accounts.VHosts.validRegexp.MatchString(vhost) {
return errVHostBadCharacters
}
return nil
@ -280,56 +187,6 @@ func hsSetHandler(server *Server, client *Client, command string, params []strin
}
}
func hsWaitingHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
requests, total := server.accounts.VHostListRequests(10)
hsNotice(rb, fmt.Sprintf(client.t("There are %[1]d pending requests for vhosts (%[2]d displayed)"), total, len(requests)))
for i, request := range requests {
hsNotice(rb, fmt.Sprintf(client.t("%[1]d. User %[2]s requests vhost: %[3]s"), i+1, request.Account, request.RequestedVHost))
}
}
func hsApproveHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
user := params[0]
vhostInfo, err := server.accounts.VHostApprove(user)
if err != nil {
hsNotice(rb, client.t("An error occurred"))
} else {
hsNotice(rb, fmt.Sprintf(client.t("Successfully approved vhost request for %s"), user))
chanMsg := fmt.Sprintf("Oper %[1]s approved vhost %[2]s for account %[3]s", client.Nick(), vhostInfo.ApprovedVHost, user)
hsNotifyChannel(server, chanMsg)
server.snomasks.Send(sno.LocalVhosts, chanMsg)
for _, client := range server.accounts.AccountToClients(user) {
client.Send(nil, hsNickMask, "NOTICE", client.Nick(), client.t("Your vhost request was approved by an administrator"))
}
}
}
func hsRejectHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var reason string
user := params[0]
if len(params) > 1 {
reason = params[1]
}
vhostInfo, err := server.accounts.VHostReject(user, reason)
if err != nil {
hsNotice(rb, client.t("An error occurred"))
} else {
hsNotice(rb, fmt.Sprintf(client.t("Successfully rejected vhost request for %s"), user))
chanMsg := fmt.Sprintf("Oper %s rejected vhost %s for account %s, with the reason: %v", client.Nick(), vhostInfo.RejectedVHost, user, reason)
hsNotifyChannel(server, chanMsg)
server.snomasks.Send(sno.LocalVhosts, chanMsg)
for _, client := range server.accounts.AccountToClients(user) {
if reason == "" {
client.Send(nil, hsNickMask, "NOTICE", client.Nick(), client.t("Your vhost request was rejected by an administrator"))
} else {
client.Send(nil, hsNickMask, "NOTICE", client.Nick(), fmt.Sprintf(client.t("Your vhost request was rejected by an administrator. The reason given was: %s"), reason))
}
}
}
}
func hsSetCloakSecretHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
secret := params[0]
expectedCode := utils.ConfirmationCode(secret, server.ctime)

View File

@ -69,8 +69,8 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
// produce a hardcoded version of the database schema
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
// version 14 db)
tx.Set(keySchemaVersion, "14", nil)
// db of the hardcoded version)
tx.Set(keySchemaVersion, "17", nil)
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
for username, userInfo := range dbImport.Users {

View File

@ -570,9 +570,6 @@ func (server *Server) applyConfig(config *Config) (err error) {
if !oldConfig.Accounts.NickReservation.Enabled {
server.accounts.buildNickToAccountIndex(config)
}
if !oldConfig.Accounts.VHosts.Enabled {
server.accounts.initVHostRequestQueue(config)
}
if !oldConfig.Channels.Registration.Enabled {
server.channels.loadRegisteredChannels(config)
}