From 05cb80507f4a2e04a4bb0875dc11b099879cf4f3 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sat, 11 Jan 2020 22:43:40 -0500 Subject: [PATCH 1/4] fix #741 --- irc/accounts.go | 13 +++++- irc/config.go | 7 ++++ irc/hostserv.go | 107 ++++++++++++++++++++++++++++++++++++++++++++++++ oragono.yaml | 4 ++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/irc/accounts.go b/irc/accounts.go index ce18984b..784d9dd3 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -1129,6 +1129,7 @@ func (am *AccountManager) ModifyAccountSettings(account string, munger settingsM type VHostInfo struct { ApprovedVHost string Enabled bool + Forbidden bool RequestedVHost string RejectedVHost string RejectionReason string @@ -1207,6 +1208,16 @@ func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result return am.performVHostChange(client.Account(), munger) } +func (am *AccountManager) VHostForbid(account string, forbid bool) (result VHostInfo, err error) { + munger := func(input VHostInfo) (output VHostInfo, err error) { + output = input + output.Forbidden = forbid + return + } + + return am.performVHostChange(account, munger) +} + func (am *AccountManager) performVHostChange(account string, munger vhostMunger) (result VHostInfo, err error) { account, err = CasefoldName(account) if err != nil || account == "" { @@ -1322,7 +1333,7 @@ func (am *AccountManager) applyVHostInfo(client *Client, info VHostInfo) { } vhost := "" - if info.Enabled { + if info.Enabled && !info.Forbidden { vhost = info.ApprovedVHost } oldNickmask := client.NickMaskString() diff --git a/irc/config.go b/irc/config.go index 93e31836..3feb27a5 100644 --- a/irc/config.go +++ b/irc/config.go @@ -117,6 +117,7 @@ type VHostConfig struct { Channel string Cooldown time.Duration } `yaml:"user-requests"` + OfferList []string `yaml:"offer-list"` } type NickEnforcementMethod int @@ -810,6 +811,12 @@ func LoadConfig(filename string) (config *Config, err error) { config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex } + for _, vhost := range config.Accounts.VHosts.OfferList { + if !config.Accounts.VHosts.ValidRegexp.MatchString(vhost) { + return nil, fmt.Errorf("invalid offered vhost: %s", vhost) + } + } + if !config.Accounts.LoginThrottling.Enabled { config.Accounts.LoginThrottling.MaxAttempts = 0 // limit of 0 means disabled } diff --git a/irc/hostserv.go b/irc/hostserv.go index 8b1242e8..89696583 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -121,6 +121,51 @@ for the rejection.`, maxParams: 2, unsplitFinalParam: true, }, + "forbid": { + handler: hsForbidHandler, + help: `Syntax: $bFORBID $b + +FORBID prevents a user from using any vhost, including ones on the offer list.`, + helpShort: `$bFORBID$b prevents a user from using vhosts.`, + capabs: []string{"vhosts"}, + enabled: hostservEnabled, + minParams: 1, + maxParams: 1, + }, + "permit": { + handler: hsForbidHandler, + help: `Syntax: $bPERMIT $b + +PERMIT undoes FORBID, allowing the user to TAKE vhosts again.`, + helpShort: `$bPERMIT$b allows a user to use vhosts again.`, + capabs: []string{"vhosts"}, + enabled: hostservEnabled, + minParams: 1, + maxParams: 1, + }, + "offerlist": { + handler: hsOfferListHandler, + help: `Syntax: $bOFFERLIST$b + +OFFERLIST lists vhosts that can be chosen without requiring operator approval; +to use one of the listed vhosts, take it with /HOSTSERV TAKE.`, + helpShort: `$bOFFERLIST$b lists vhosts that can be taken without operator approval.`, + enabled: hostservEnabled, + minParams: 0, + maxParams: 0, + }, + "take": { + handler: hsTakeHandler, + help: `Syntax: $bTAKE$b + +TAKE sets your vhost to one of the vhosts in the server's offer list; to see +the offered vhosts, use /HOSTSERV OFFERLIST.`, + helpShort: `$bTAKE$b sets your vhost to one of the options from the offer list.`, + enabled: hostservEnabled, + authRequired: true, + minParams: 1, + maxParams: 1, + }, } ) @@ -218,6 +263,11 @@ func hsStatusHandler(server *Server, client *Client, command string, params []st return } + if account.VHost.Forbidden { + hsNotice(rb, client.t("An administrator has denied you the ability to use vhosts")) + return + } + if account.VHost.ApprovedVHost != "" { hsNotice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost)) if !account.VHost.Enabled { @@ -316,3 +366,60 @@ func hsRejectHandler(server *Server, client *Client, command string, params []st } } } + +func hsForbidHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + user := params[0] + forbidden := command == "forbid" + + _, err := server.accounts.VHostForbid(user, forbidden) + if err == errAccountDoesNotExist { + hsNotice(rb, client.t("No such account")) + } else if err != nil { + hsNotice(rb, client.t("An error occurred")) + } else { + if forbidden { + hsNotice(rb, fmt.Sprintf(client.t("Successfully forbidden vhosts to user %s"), user)) + } else { + hsNotice(rb, fmt.Sprintf(client.t("Successfully permitted vhosts for user %s"), user)) + } + } +} + +func hsOfferListHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + vhostConfig := server.Config().Accounts.VHosts + if len(vhostConfig.OfferList) == 0 { + if vhostConfig.UserRequests.Enabled { + hsNotice(rb, client.t("The server does not offer any vhosts (but you can request one with /HOSTSERV REQUEST)")) + } else { + hsNotice(rb, client.t("The server does not offer any vhosts)")) + } + } else { + hsNotice(rb, client.t("The following vhosts are available and can be chosen with /HOSTSERV TAKE:")) + for i, vhost := range vhostConfig.OfferList { + hsNotice(rb, fmt.Sprintf("%d. %s", i+1, vhost)) + } + } +} + +func hsTakeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + vhost := params[0] + found := false + for _, offered := range server.Config().Accounts.VHosts.OfferList { + if offered == vhost { + found = true + } + } + if !found { + hsNotice(rb, client.t("That vhost isn't being offered by the server")) + return + } + + _, err := server.accounts.VHostSet(client.Account(), vhost) + if err != nil { + hsNotice(rb, client.t("An error occurred")) + } else if vhost != "" { + hsNotice(rb, client.t("Successfully set vhost")) + } else { + hsNotice(rb, client.t("Successfully cleared vhost")) + } +} diff --git a/oragono.yaml b/oragono.yaml index 70f24754..61137587 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -380,6 +380,10 @@ accounts: # before they can request a new one. cooldown: 168h + # vhosts that users can take without approval, using `/HS TAKE` + offer-list: + #- "oragono.test" + # channel options channels: # modes that are set when new channels are created From 2db14c91b90438d7654aa32e30fffab5242f0178 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sat, 11 Jan 2020 22:52:05 -0500 Subject: [PATCH 2/4] review fixes --- irc/hostserv.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/irc/hostserv.go b/irc/hostserv.go index 89696583..8c53791c 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -391,12 +391,12 @@ func hsOfferListHandler(server *Server, client *Client, command string, params [ if vhostConfig.UserRequests.Enabled { hsNotice(rb, client.t("The server does not offer any vhosts (but you can request one with /HOSTSERV REQUEST)")) } else { - hsNotice(rb, client.t("The server does not offer any vhosts)")) + hsNotice(rb, client.t("The server does not offer any vhosts")) } } else { hsNotice(rb, client.t("The following vhosts are available and can be chosen with /HOSTSERV TAKE:")) - for i, vhost := range vhostConfig.OfferList { - hsNotice(rb, fmt.Sprintf("%d. %s", i+1, vhost)) + for _, vhost := range vhostConfig.OfferList { + hsNotice(rb, vhost) } } } From dedf78d0e934f5832bb8340156c75a20c91c49ea Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 28 Jan 2020 21:23:31 -0500 Subject: [PATCH 3/4] review fixes --- irc/hostserv.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/irc/hostserv.go b/irc/hostserv.go index 8c53791c..0813d82d 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -378,9 +378,9 @@ func hsForbidHandler(server *Server, client *Client, command string, params []st hsNotice(rb, client.t("An error occurred")) } else { if forbidden { - hsNotice(rb, fmt.Sprintf(client.t("Successfully forbidden vhosts to user %s"), user)) + hsNotice(rb, fmt.Sprintf(client.t("User %s is no longer allowed to use vhosts"), user)) } else { - hsNotice(rb, fmt.Sprintf(client.t("Successfully permitted vhosts for user %s"), user)) + hsNotice(rb, fmt.Sprintf(client.t("User %s is now allowed to use vhosts"), user)) } } } @@ -389,7 +389,7 @@ func hsOfferListHandler(server *Server, client *Client, command string, params [ vhostConfig := server.Config().Accounts.VHosts if len(vhostConfig.OfferList) == 0 { if vhostConfig.UserRequests.Enabled { - hsNotice(rb, client.t("The server does not offer any vhosts (but you can request one with /HOSTSERV REQUEST)")) + hsNotice(rb, client.t("The server does not offer any vhosts, but you can request one with /HOSTSERV REQUEST")) } else { hsNotice(rb, client.t("The server does not offer any vhosts")) } From 955cdbdfef052fb0ea6e8ab24f4df2e47af1d0a7 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 28 Jan 2020 22:27:56 -0500 Subject: [PATCH 4/4] impose throttle checks on HS TAKE --- irc/accounts.go | 40 ++++++++++++++++++++++++++++++++++++++-- irc/hostserv.go | 36 +++++++++++++++--------------------- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index 784d9dd3..f926e9c1 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -1142,12 +1142,40 @@ type PendingVHostRequest struct { 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 type vhostMunger func(input VHostInfo) (output VHostInfo, err error) -func (am *AccountManager) VHostSet(account string, vhost string) (result VHostInfo, err error) { +func (am *AccountManager) VHostSet(account string, vhost string, cooldown time.Duration) (result VHostInfo, err error) { munger := func(input VHostInfo) (output VHostInfo, err error) { output = input + err = output.checkThrottle(cooldown) + if err != nil { + return + } output.Enabled = true output.ApprovedVHost = vhost return @@ -1156,9 +1184,17 @@ func (am *AccountManager) VHostSet(account string, vhost string) (result VHostIn return am.performVHostChange(account, munger) } -func (am *AccountManager) VHostRequest(account string, vhost string) (result VHostInfo, err error) { +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 = "" diff --git a/irc/hostserv.go b/irc/hostserv.go index 0813d82d..8cfa9220 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "regexp" - "time" ) const hostservHelp = `HostServ lets you manage your vhost (i.e., the string displayed @@ -213,23 +212,13 @@ func hsRequestHandler(server *Server, client *Client, command string, params []s } accountName := client.Account() - account, err := server.accounts.LoadAccount(client.Account()) + _, err := server.accounts.VHostRequest(accountName, vhost, server.Config().Accounts.VHosts.UserRequests.Cooldown) if err != nil { - hsNotice(rb, client.t("An error occurred")) - return - } - elapsed := time.Since(account.VHost.LastRequestTime) - remainingTime := server.AccountConfig().VHosts.UserRequests.Cooldown - elapsed - // you can update your existing request, but if you were rejected, - // you can't spam a replacement request - if account.VHost.RequestedVHost == "" && remainingTime > 0 { - hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), remainingTime)) - return - } - - _, err = server.accounts.VHostRequest(accountName, vhost) - if err != nil { - hsNotice(rb, client.t("An error occurred")) + 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) @@ -309,7 +298,7 @@ func hsSetHandler(server *Server, client *Client, command string, params []strin } // else: command == "del", vhost == "" - _, err := server.accounts.VHostSet(user, vhost) + _, err := server.accounts.VHostSet(user, vhost, 0) if err != nil { hsNotice(rb, client.t("An error occurred")) } else if vhost != "" { @@ -402,9 +391,10 @@ func hsOfferListHandler(server *Server, client *Client, command string, params [ } func hsTakeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + config := server.Config() vhost := params[0] found := false - for _, offered := range server.Config().Accounts.VHosts.OfferList { + for _, offered := range config.Accounts.VHosts.OfferList { if offered == vhost { found = true } @@ -414,9 +404,13 @@ func hsTakeHandler(server *Server, client *Client, command string, params []stri return } - _, err := server.accounts.VHostSet(client.Account(), vhost) + _, err := server.accounts.VHostSet(client.Account(), vhost, config.Accounts.VHosts.UserRequests.Cooldown) if err != nil { - hsNotice(rb, client.t("An error occurred")) + if throttled, ok := err.(*vhostThrottleExceeded); ok { + hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before taking a vhost"), throttled.timeRemaining)) + } else { + hsNotice(rb, client.t("An error occurred")) + } } else if vhost != "" { hsNotice(rb, client.t("Successfully set vhost")) } else {