ircd/irc/hostserv.go

460 lines
15 KiB
Go
Raw Normal View History

2018-04-23 06:38:35 +00:00
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2018-04-19 06:48:19 +00:00
// released under the MIT license
package irc
import (
"errors"
"fmt"
"regexp"
"time"
2020-02-02 07:03:08 +00:00
2020-05-08 05:16:49 +00:00
"github.com/goshuirc/irc-go/ircfmt"
2020-02-02 07:03:08 +00:00
"github.com/oragono/oragono/irc/sno"
2020-05-08 05:16:49 +00:00
"github.com/oragono/oragono/irc/utils"
2018-04-19 06:48:19 +00:00
)
2020-03-18 10:01:19 +00:00
const (
hostservHelp = `HostServ lets you manage your vhost (i.e., the string displayed
in place of your client's hostname/IP).`
2020-03-18 10:01:19 +00:00
hsNickMask = "HostServ!HostServ@localhost"
)
2018-04-19 06:48:19 +00:00
var (
errVHostBadCharacters = errors.New("Vhost contains prohibited characters")
errVHostTooLong = errors.New("Vhost is too long")
// ascii only for now
2018-04-23 06:38:35 +00:00
defaultValidVhostRegex = regexp.MustCompile(`^[0-9A-Za-z.\-_/]+$`)
2018-04-19 06:48:19 +00:00
)
2019-01-04 03:32:07 +00:00
func hostservEnabled(config *Config) bool {
return config.Accounts.VHosts.Enabled
2018-04-19 06:48:19 +00:00
}
2019-01-04 03:32:07 +00:00
func hostservRequestsEnabled(config *Config) bool {
return config.Accounts.VHosts.Enabled && config.Accounts.VHosts.UserRequests.Enabled
2018-04-19 06:48:19 +00:00
}
var (
hostservCommands = map[string]*serviceCommand{
"on": {
handler: hsOnOffHandler,
help: `Syntax: $bON$b
ON enables your vhost, if you have one approved.`,
helpShort: `$bON$b enables your vhost, if you have one approved.`,
authRequired: true,
enabled: hostservEnabled,
},
"off": {
handler: hsOnOffHandler,
help: `Syntax: $bOFF$b
OFF disables your vhost, if you have one approved.`,
helpShort: `$bOFF$b 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,
2019-01-04 03:32:07 +00:00
minParams: 1,
2018-04-19 06:48:19 +00:00
},
"status": {
handler: hsStatusHandler,
2019-01-04 03:32:07 +00:00
help: `Syntax: $bSTATUS [user]$b
2018-04-19 06:48:19 +00:00
STATUS displays your current vhost, if any, and the status of your most recent
2019-01-04 03:32:07 +00:00
request for a new one. A server operator can view someone else's status.`,
helpShort: `$bSTATUS$b shows your vhost and request status.`,
enabled: hostservEnabled,
2018-04-19 06:48:19 +00:00
},
"set": {
handler: hsSetHandler,
help: `Syntax: $bSET <user> <vhost>$b
SET sets a user's vhost, bypassing the request system.`,
helpShort: `$bSET$b sets a user's vhost.`,
2018-04-23 06:38:35 +00:00
capabs: []string{"vhosts"},
2018-04-19 06:48:19 +00:00
enabled: hostservEnabled,
2019-01-04 03:32:07 +00:00
minParams: 2,
2018-04-19 06:48:19 +00:00
},
"del": {
handler: hsSetHandler,
help: `Syntax: $bDEL <user>$b
2018-12-28 16:07:08 +00:00
DEL deletes a user's vhost.`,
2018-04-19 06:48:19 +00:00
helpShort: `$bDEL$b deletes a user's vhost.`,
2018-04-23 06:38:35 +00:00
capabs: []string{"vhosts"},
2018-04-19 06:48:19 +00:00
enabled: hostservEnabled,
2019-01-04 03:32:07 +00:00
minParams: 1,
2018-04-19 06:48:19 +00:00
},
"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.`,
2018-04-23 06:38:35 +00:00
capabs: []string{"vhosts"},
2018-04-19 06:48:19 +00:00
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.`,
2018-04-23 06:38:35 +00:00
capabs: []string{"vhosts"},
2018-04-19 06:48:19 +00:00
enabled: hostservEnabled,
2019-01-04 03:32:07 +00:00
minParams: 1,
2018-04-19 06:48:19 +00:00
},
"reject": {
handler: hsRejectHandler,
help: `Syntax: $bREJECT <user> [<reason>]$b
REJECT rejects a user's vhost request, optionally giving them a reason
for the rejection.`,
2019-05-29 08:25:20 +00:00
helpShort: `$bREJECT$b rejects a user's vhost request.`,
capabs: []string{"vhosts"},
enabled: hostservEnabled,
minParams: 1,
maxParams: 2,
unsplitFinalParam: true,
2018-04-19 06:48:19 +00:00
},
2020-01-12 03:43:40 +00:00
"forbid": {
handler: hsForbidHandler,
help: `Syntax: $bFORBID <user>$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 <user>$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 <vhost>
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,
},
2020-05-08 05:16:49 +00:00
"setcloaksecret": {
handler: hsSetCloakSecretHandler,
help: `Syntax: $bSETCLOAKSECRET$b <secret> [code]
SETCLOAKSECRET can be used to set or rotate the cloak secret. You should use
a cryptographically strong secret. To prevent accidental modification, a
verification code is required; invoking the command without a code will
display the necessary code.`,
helpShort: `$bSETCLOAKSECRET$b modifies the IP cloaking secret.`,
capabs: []string{"vhosts", "rehash"},
minParams: 1,
maxParams: 2,
},
2018-04-19 06:48:19 +00:00
}
)
// hsNotice sends the client a notice from HostServ
func hsNotice(rb *ResponseBuffer, text string) {
2020-03-18 10:01:19 +00:00
rb.Add(nil, hsNickMask, "NOTICE", rb.target.Nick(), text)
2018-04-19 06:48:19 +00:00
}
2018-04-23 06:38:35 +00:00
// hsNotifyChannel notifies the designated channel of new vhost activity
func hsNotifyChannel(server *Server, message string) {
2020-03-16 11:54:50 +00:00
chname := server.Config().Accounts.VHosts.UserRequests.Channel
2018-04-23 06:38:35 +00:00
channel := server.channels.Get(chname)
if channel == nil {
return
}
chname = channel.Name()
for _, client := range channel.Members() {
2020-03-18 10:01:19 +00:00
client.Send(nil, hsNickMask, "PRIVMSG", chname, message)
2018-04-23 06:38:35 +00:00
}
}
2019-01-04 03:32:07 +00:00
func hsOnOffHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
2018-04-19 06:48:19 +00:00
enable := false
if command == "on" {
enable = true
}
2018-04-23 06:38:35 +00:00
_, err := server.accounts.VHostSetEnabled(client, enable)
2019-05-22 03:55:04 +00:00
if err == errNoVhost {
hsNotice(rb, client.t(err.Error()))
} else if err != nil {
2018-04-19 06:48:19 +00:00
hsNotice(rb, client.t("An error occurred"))
} else if enable {
hsNotice(rb, client.t("Successfully enabled your vhost"))
} else {
hsNotice(rb, client.t("Successfully disabled your vhost"))
}
}
2019-01-04 03:32:07 +00:00
func hsRequestHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
vhost := params[0]
2018-04-19 06:48:19 +00:00
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))
2018-04-19 06:48:19 +00:00
if err != nil {
2020-01-29 03:27:56 +00:00
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 if err == errVhostsForbidden {
hsNotice(rb, client.t("An administrator has denied you the ability to use vhosts"))
2020-01-29 03:27:56 +00:00
} else {
hsNotice(rb, client.t("An error occurred"))
}
2018-04-19 06:48:19 +00:00
} else {
hsNotice(rb, client.t("Your vhost request will be reviewed by an administrator"))
2018-04-23 06:38:35 +00:00
chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost)
hsNotifyChannel(server, chanMsg)
2020-02-02 07:03:08 +00:00
server.snomasks.Send(sno.LocalVhosts, chanMsg)
2018-04-19 06:48:19 +00:00
}
}
2019-01-04 03:32:07 +00:00
func hsStatusHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
var accountName string
if len(params) > 0 {
if !client.HasRoleCapabs("vhosts") {
hsNotice(rb, client.t("Command restricted"))
return
}
accountName = params[0]
} else {
accountName = client.Account()
2019-02-14 18:10:35 +00:00
if accountName == "" {
hsNotice(rb, client.t("You're not logged into an account"))
return
}
2019-01-04 03:32:07 +00:00
}
2018-04-19 06:48:19 +00:00
account, err := server.accounts.LoadAccount(accountName)
if err != nil {
2019-01-04 03:32:07 +00:00
if err != errAccountDoesNotExist {
server.logger.Warning("internal", "error loading account info", accountName, err.Error())
}
hsNotice(rb, client.t("No such account"))
2018-04-19 06:48:19 +00:00
return
}
2020-01-12 03:43:40 +00:00
if account.VHost.Forbidden {
hsNotice(rb, client.t("An administrator has denied you the ability to use vhosts"))
return
}
2018-04-19 06:48:19 +00:00
if account.VHost.ApprovedVHost != "" {
hsNotice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost))
2018-04-19 06:48:19 +00:00
if !account.VHost.Enabled {
hsNotice(rb, client.t("This vhost is currently disabled, but can be enabled with /HS ON"))
2018-04-19 06:48:19 +00:00
}
} 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 {
2020-03-16 11:54:50 +00:00
config := server.Config()
if len(vhost) > config.Accounts.VHosts.MaxLength {
2018-04-19 06:48:19 +00:00
return errVHostTooLong
}
2020-03-16 11:54:50 +00:00
if !config.Accounts.VHosts.ValidRegexp.MatchString(vhost) {
2018-04-19 06:48:19 +00:00
return errVHostBadCharacters
}
return nil
}
2019-01-04 03:32:07 +00:00
func hsSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
user := params[0]
var vhost string
2018-04-19 06:48:19 +00:00
if command == "set" {
2019-01-04 03:32:07 +00:00
vhost = params[1]
2018-04-19 06:48:19 +00:00
if validateVhost(server, vhost, true) != nil {
hsNotice(rb, client.t("Invalid vhost"))
return
}
}
2019-01-04 03:32:07 +00:00
// else: command == "del", vhost == ""
2018-04-19 06:48:19 +00:00
2020-02-02 03:19:33 +00:00
_, err := server.accounts.VHostSet(user, vhost)
2018-04-19 06:48:19 +00:00
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"))
}
}
2019-01-04 03:32:07 +00:00
func hsWaitingHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
2018-04-19 06:48:19 +00:00
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)))
2018-04-19 06:48:19 +00:00
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))
2018-04-19 06:48:19 +00:00
}
}
2019-01-04 03:32:07 +00:00
func hsApproveHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
user := params[0]
2018-04-19 06:48:19 +00:00
2018-04-23 06:38:35 +00:00
vhostInfo, err := server.accounts.VHostApprove(user)
2018-04-19 06:48:19 +00:00
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)
2018-04-23 06:38:35 +00:00
hsNotifyChannel(server, chanMsg)
2020-02-02 07:03:08 +00:00
server.snomasks.Send(sno.LocalVhosts, chanMsg)
2018-04-19 06:48:19 +00:00
for _, client := range server.accounts.AccountToClients(user) {
2020-03-18 10:01:19 +00:00
client.Send(nil, hsNickMask, "NOTICE", client.Nick(), client.t("Your vhost request was approved by an administrator"))
2018-04-19 06:48:19 +00:00
}
}
}
2019-01-04 03:32:07 +00:00
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]
2018-04-19 06:48:19 +00:00
}
2018-04-23 06:38:35 +00:00
vhostInfo, err := server.accounts.VHostReject(user, reason)
2018-04-19 06:48:19 +00:00
if err != nil {
hsNotice(rb, client.t("An error occurred"))
} else {
hsNotice(rb, fmt.Sprintf(client.t("Successfully rejected vhost request for %s"), user))
2018-04-23 06:38:35 +00:00
chanMsg := fmt.Sprintf("Oper %s rejected vhost %s for account %s, with the reason: %v", client.Nick(), vhostInfo.RejectedVHost, user, reason)
hsNotifyChannel(server, chanMsg)
2020-02-02 07:03:08 +00:00
server.snomasks.Send(sno.LocalVhosts, chanMsg)
2018-04-19 06:48:19 +00:00
for _, client := range server.accounts.AccountToClients(user) {
if reason == "" {
2020-03-18 10:01:19 +00:00
client.Send(nil, hsNickMask, "NOTICE", client.Nick(), client.t("Your vhost request was rejected by an administrator"))
2018-04-19 06:48:19 +00:00
} else {
2020-03-18 10:01:19 +00:00
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))
2018-04-19 06:48:19 +00:00
}
}
}
}
2020-01-12 03:43:40 +00:00
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 {
2020-01-29 02:23:31 +00:00
hsNotice(rb, fmt.Sprintf(client.t("User %s is no longer allowed to use vhosts"), user))
2020-01-12 03:43:40 +00:00
} else {
2020-01-29 02:23:31 +00:00
hsNotice(rb, fmt.Sprintf(client.t("User %s is now allowed to use vhosts"), user))
2020-01-12 03:43:40 +00:00
}
}
}
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 {
2020-01-29 02:23:31 +00:00
hsNotice(rb, client.t("The server does not offer any vhosts, but you can request one with /HOSTSERV REQUEST"))
2020-01-12 03:43:40 +00:00
} else {
2020-01-12 03:52:05 +00:00
hsNotice(rb, client.t("The server does not offer any vhosts"))
2020-01-12 03:43:40 +00:00
}
} else {
hsNotice(rb, client.t("The following vhosts are available and can be chosen with /HOSTSERV TAKE:"))
2020-01-12 03:52:05 +00:00
for _, vhost := range vhostConfig.OfferList {
hsNotice(rb, vhost)
2020-01-12 03:43:40 +00:00
}
}
}
func hsTakeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
2020-01-29 03:27:56 +00:00
config := server.Config()
2020-01-12 03:43:40 +00:00
vhost := params[0]
found := false
2020-01-29 03:27:56 +00:00
for _, offered := range config.Accounts.VHosts.OfferList {
2020-01-12 03:43:40 +00:00
if offered == vhost {
found = true
}
}
if !found {
hsNotice(rb, client.t("That vhost isn't being offered by the server"))
return
}
2020-02-02 07:03:08 +00:00
account := client.Account()
_, err := server.accounts.VHostTake(account, vhost, time.Duration(config.Accounts.VHosts.UserRequests.Cooldown))
2020-01-12 03:43:40 +00:00
if err != nil {
2020-01-29 03:27:56 +00:00
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 if err == errVhostsForbidden {
hsNotice(rb, client.t("An administrator has denied you the ability to use vhosts"))
2020-01-29 03:27:56 +00:00
} else {
hsNotice(rb, client.t("An error occurred"))
}
2020-01-12 03:43:40 +00:00
} else {
2020-02-02 07:03:08 +00:00
hsNotice(rb, client.t("Successfully set vhost"))
server.snomasks.Send(sno.LocalVhosts, fmt.Sprintf("Client %s (account %s) took vhost %s", client.Nick(), account, vhost))
2020-01-12 03:43:40 +00:00
}
}
2020-05-08 05:16:49 +00:00
func hsSetCloakSecretHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
secret := params[0]
expectedCode := utils.ConfirmationCode(secret, server.ctime)
if len(params) == 1 || params[1] != expectedCode {
hsNotice(rb, ircfmt.Unescape(client.t("$bWarning: changing the cloak secret will invalidate stored ban/invite/exception lists.$b")))
2020-05-08 06:47:08 +00:00
hsNotice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
2020-05-08 05:16:49 +00:00
return
}
StoreCloakSecret(server.store, secret)
hsNotice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
}