mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 13:29:27 +01:00
Merge pull request #247 from slingamn/vhosts.3
initial vhosts implementation, #183
This commit is contained in:
commit
de7b679fc5
333
irc/accounts.go
333
irc/accounts.go
@ -14,6 +14,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
@ -30,14 +31,24 @@ const (
|
||||
keyAccountRegTime = "account.registered.time %s"
|
||||
keyAccountCredentials = "account.credentials %s"
|
||||
keyAccountAdditionalNicks = "account.additionalnicks %s"
|
||||
keyAccountVHost = "account.vhost %s"
|
||||
keyCertToAccount = "account.creds.certfp %s"
|
||||
|
||||
keyVHostQueueAcctToId = "vhostQueue %s"
|
||||
vhostRequestIdx = "vhostQueue"
|
||||
)
|
||||
|
||||
// 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
|
||||
@ -53,6 +64,7 @@ func NewAccountManager(server *Server) *AccountManager {
|
||||
}
|
||||
|
||||
am.buildNickToAccountIndex()
|
||||
am.initVHostRequestQueue()
|
||||
return &am
|
||||
}
|
||||
|
||||
@ -94,8 +106,44 @@ func (am *AccountManager) buildNickToAccountIndex() {
|
||||
am.nickToAccount = result
|
||||
am.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
func (am *AccountManager) initVHostRequestQueue() {
|
||||
if !am.server.AccountConfig().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 {
|
||||
@ -109,6 +157,17 @@ func (am *AccountManager) NickToAccount(nick string) string {
|
||||
return am.nickToAccount[cfnick]
|
||||
}
|
||||
|
||||
func (am *AccountManager) AccountToClients(account string) (result []*Client) {
|
||||
cfaccount, err := CasefoldName(account)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
am.RLock()
|
||||
defer am.RUnlock()
|
||||
return am.accountToClients[cfaccount]
|
||||
}
|
||||
|
||||
func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
if err != nil || account == "" || account == "*" {
|
||||
@ -342,7 +401,12 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
return err
|
||||
}
|
||||
|
||||
am.Login(client, raw.Name)
|
||||
raw.Verified = true
|
||||
clientAccount, err := am.deserializeRawAccount(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
am.Login(client, clientAccount)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -464,7 +528,7 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
|
||||
return errAccountInvalidCredentials
|
||||
}
|
||||
|
||||
am.Login(client, account.Name)
|
||||
am.Login(client, account)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -484,6 +548,11 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
|
||||
return
|
||||
}
|
||||
|
||||
result, err = am.deserializeRawAccount(raw)
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result ClientAccount, err error) {
|
||||
result.Name = raw.Name
|
||||
regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
|
||||
result.RegisteredAt = time.Unix(regTimeInt, 0)
|
||||
@ -495,6 +564,13 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
|
||||
}
|
||||
result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
|
||||
result.Verified = raw.Verified
|
||||
if raw.VHost != "" {
|
||||
e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
|
||||
if e != nil {
|
||||
am.server.logger.Warning("internal", fmt.Sprintf("could not unmarshal vhost for account %s: %v", result.Name, e))
|
||||
// pretend they have no vhost and move on
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -506,6 +582,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
||||
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||
|
||||
_, e := tx.Get(accountKey)
|
||||
if e == buntdb.ErrNotFound {
|
||||
@ -518,6 +595,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
||||
result.Credentials, _ = tx.Get(credentialsKey)
|
||||
result.Callback, _ = tx.Get(callbackKey)
|
||||
result.AdditionalNicks, _ = tx.Get(nicksKey)
|
||||
result.VHost, _ = tx.Get(vhostKey)
|
||||
|
||||
if _, e = tx.Get(verifiedKey); e == nil {
|
||||
result.Verified = true
|
||||
@ -540,6 +618,8 @@ func (am *AccountManager) Unregister(account string) error {
|
||||
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
||||
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||
vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
|
||||
|
||||
var clients []*Client
|
||||
|
||||
@ -560,6 +640,9 @@ func (am *AccountManager) Unregister(account string) error {
|
||||
tx.Delete(nicksKey)
|
||||
credText, err = tx.Get(credentialsKey)
|
||||
tx.Delete(credentialsKey)
|
||||
tx.Delete(vhostKey)
|
||||
_, err := tx.Delete(vhostQueueKey)
|
||||
am.decrementVHostQueueCount(casefoldedAccount, err)
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -624,17 +707,239 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
|
||||
}
|
||||
|
||||
// ok, we found an account corresponding to their certificate
|
||||
|
||||
am.Login(client, rawAccount.Name)
|
||||
clientAccount, err := am.deserializeRawAccount(rawAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
am.Login(client, clientAccount)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) Login(client *Client, account string) {
|
||||
// represents someone's status in hostserv
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
munger := func(input VHostInfo) (output VHostInfo, err error) {
|
||||
output = input
|
||||
output.Enabled = true
|
||||
output.ApprovedVHost = vhost
|
||||
return
|
||||
}
|
||||
|
||||
return am.performVHostChange(account, munger)
|
||||
}
|
||||
|
||||
func (am *AccountManager) VHostRequest(account string, vhost string) (result VHostInfo, err error) {
|
||||
munger := func(input VHostInfo) (output VHostInfo, err error) {
|
||||
output = input
|
||||
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) {
|
||||
output = input
|
||||
output.Enabled = enabled
|
||||
return
|
||||
}
|
||||
|
||||
return am.performVHostChange(client.Account(), munger)
|
||||
}
|
||||
|
||||
func (am *AccountManager) performVHostChange(account string, munger vhostMunger) (result VHostInfo, err error) {
|
||||
account, err = CasefoldName(account)
|
||||
if err != nil || account == "" {
|
||||
err = errAccountDoesNotExist
|
||||
return
|
||||
}
|
||||
|
||||
am.vHostUpdateMutex.Lock()
|
||||
defer am.vHostUpdateMutex.Unlock()
|
||||
|
||||
clientAccount, err := am.LoadAccount(account)
|
||||
if err != nil {
|
||||
err = errAccountDoesNotExist
|
||||
return
|
||||
} else if !clientAccount.Verified {
|
||||
err = errAccountUnverified
|
||||
return
|
||||
}
|
||||
|
||||
result, err = munger(clientAccount.VHost)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vhtext, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
err = errAccountUpdateFailed
|
||||
return
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
err = errAccountUpdateFailed
|
||||
return
|
||||
}
|
||||
|
||||
am.applyVhostToClients(account, result)
|
||||
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
|
||||
if !am.server.AccountConfig().VHosts.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
vhost := ""
|
||||
if info.Enabled {
|
||||
vhost = info.ApprovedVHost
|
||||
}
|
||||
oldNickmask := client.NickMaskString()
|
||||
updated := client.SetVHost(vhost)
|
||||
if updated {
|
||||
// TODO: doing I/O here is kind of a kludge
|
||||
go client.sendChghost(oldNickmask, vhost)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) applyVhostToClients(account string, result VHostInfo) {
|
||||
am.RLock()
|
||||
clients := am.accountToClients[account]
|
||||
am.RUnlock()
|
||||
|
||||
for _, client := range clients {
|
||||
am.applyVHostInfo(client, result)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) Login(client *Client, account ClientAccount) {
|
||||
changed := client.SetAccountName(account.Name)
|
||||
if changed {
|
||||
go client.nickTimer.Touch()
|
||||
}
|
||||
|
||||
am.applyVHostInfo(client, account.VHost)
|
||||
|
||||
casefoldedAccount := client.Account()
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
|
||||
am.loginToAccount(client, account)
|
||||
casefoldedAccount := client.Account()
|
||||
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
|
||||
}
|
||||
|
||||
@ -691,6 +996,7 @@ type ClientAccount struct {
|
||||
Credentials AccountCredentials
|
||||
Verified bool
|
||||
AdditionalNicks []string
|
||||
VHost VHostInfo
|
||||
}
|
||||
|
||||
// convenience for passing around raw serialized account data
|
||||
@ -701,14 +1007,7 @@ type rawClientAccount struct {
|
||||
Callback string
|
||||
Verified bool
|
||||
AdditionalNicks string
|
||||
}
|
||||
|
||||
// loginToAccount logs the client into the given account.
|
||||
func (am *AccountManager) loginToAccount(client *Client, account string) {
|
||||
changed := client.SetAccountName(account)
|
||||
if changed {
|
||||
go client.nickTimer.Touch()
|
||||
}
|
||||
VHost string
|
||||
}
|
||||
|
||||
// logoutOfAccount logs the client out of their current account.
|
||||
|
121
irc/chanserv.go
121
irc/chanserv.go
@ -5,7 +5,6 @@ package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
@ -22,29 +21,16 @@ To see in-depth help for a specific ChanServ command, try:
|
||||
Here are the commands you can use:
|
||||
%s`
|
||||
|
||||
type csCommand struct {
|
||||
capabs []string // oper capabs the given user has to have to access this command
|
||||
handler func(server *Server, client *Client, command, params string, rb *ResponseBuffer)
|
||||
help string
|
||||
helpShort string
|
||||
oper bool // true if the user has to be an oper to use this command
|
||||
}
|
||||
|
||||
var (
|
||||
chanservCommands = map[string]*csCommand{
|
||||
"help": {
|
||||
help: `Syntax: $bHELP [command]$b
|
||||
|
||||
HELP returns information on the given command.`,
|
||||
helpShort: `$bHELP$b shows in-depth information about commands.`,
|
||||
},
|
||||
chanservCommands = map[string]*serviceCommand{
|
||||
"op": {
|
||||
handler: csOpHandler,
|
||||
help: `Syntax: $bOP #channel [nickname]$b
|
||||
|
||||
OP makes the given nickname, or yourself, a channel admin. You can only use
|
||||
this command if you're the founder of the channel.`,
|
||||
helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`,
|
||||
helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`,
|
||||
authRequired: true,
|
||||
},
|
||||
"register": {
|
||||
handler: csRegisterHandler,
|
||||
@ -53,7 +39,8 @@ this command if you're the founder of the channel.`,
|
||||
REGISTER lets you own the given channel. If you rejoin this channel, you'll be
|
||||
given admin privs on it. Modes set on the channel and the topic will also be
|
||||
remembered.`,
|
||||
helpShort: `$bREGISTER$b lets you own a given channel.`,
|
||||
helpShort: `$bREGISTER$b lets you own a given channel.`,
|
||||
authRequired: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
@ -63,91 +50,6 @@ func csNotice(rb *ResponseBuffer, text string) {
|
||||
rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text)
|
||||
}
|
||||
|
||||
// chanservReceiveNotice handles NOTICEs that ChanServ receives.
|
||||
func (server *Server) chanservNoticeHandler(client *Client, message string, rb *ResponseBuffer) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// chanservReceiveNotice handles NOTICEs that ChanServ receives.
|
||||
func (server *Server) chanservPrivmsgHandler(client *Client, message string, rb *ResponseBuffer) {
|
||||
commandName, params := utils.ExtractParam(message)
|
||||
commandName = strings.ToLower(commandName)
|
||||
|
||||
commandInfo := chanservCommands[commandName]
|
||||
if commandInfo == nil {
|
||||
csNotice(rb, client.t("Unknown command. To see available commands, run /CS HELP"))
|
||||
return
|
||||
}
|
||||
|
||||
if commandInfo.oper && !client.HasMode(modes.Operator) {
|
||||
csNotice(rb, client.t("Command restricted"))
|
||||
return
|
||||
}
|
||||
|
||||
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
|
||||
csNotice(rb, client.t("Command restricted"))
|
||||
return
|
||||
}
|
||||
|
||||
// custom help handling here to prevent recursive init loop
|
||||
if commandName == "help" {
|
||||
csHelpHandler(server, client, commandName, params, rb)
|
||||
return
|
||||
}
|
||||
|
||||
if commandInfo.handler == nil {
|
||||
csNotice(rb, client.t("Command error. Please report this to the developers"))
|
||||
return
|
||||
}
|
||||
|
||||
server.logger.Debug("chanserv", fmt.Sprintf("Client %s ran command %s", client.Nick(), commandName))
|
||||
|
||||
commandInfo.handler(server, client, commandName, params, rb)
|
||||
}
|
||||
|
||||
func csHelpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
csNotice(rb, ircfmt.Unescape(client.t("*** $bChanServ HELP$b ***")))
|
||||
|
||||
if params == "" {
|
||||
// show general help
|
||||
var shownHelpLines sort.StringSlice
|
||||
for _, commandInfo := range chanservCommands {
|
||||
// skip commands user can't access
|
||||
if commandInfo.oper && !client.HasMode(modes.Operator) {
|
||||
continue
|
||||
}
|
||||
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
|
||||
continue
|
||||
}
|
||||
|
||||
shownHelpLines = append(shownHelpLines, " "+client.t(commandInfo.helpShort))
|
||||
}
|
||||
|
||||
// sort help lines
|
||||
sort.Sort(shownHelpLines)
|
||||
|
||||
// assemble help text
|
||||
assembledHelpLines := strings.Join(shownHelpLines, "\n")
|
||||
fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(chanservHelp), assembledHelpLines))
|
||||
|
||||
// push out help text
|
||||
for _, line := range strings.Split(fullHelp, "\n") {
|
||||
csNotice(rb, line)
|
||||
}
|
||||
} else {
|
||||
commandInfo := chanservCommands[strings.ToLower(strings.TrimSpace(params))]
|
||||
if commandInfo == nil {
|
||||
csNotice(rb, client.t("Unknown command. To see available commands, run /CS HELP"))
|
||||
} else {
|
||||
for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
|
||||
csNotice(rb, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
csNotice(rb, ircfmt.Unescape(client.t("*** $bEnd of ChanServ HELP$b ***")))
|
||||
}
|
||||
|
||||
func csOpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
channelName, clientToOp := utils.ExtractParam(params)
|
||||
|
||||
@ -171,13 +73,7 @@ func csOpHandler(server *Server, client *Client, command, params string, rb *Res
|
||||
}
|
||||
|
||||
clientAccount := client.Account()
|
||||
|
||||
if clientAccount == "" {
|
||||
csNotice(rb, client.t("You must be logged in to op on a channel"))
|
||||
return
|
||||
}
|
||||
|
||||
if clientAccount != channelInfo.Founder() {
|
||||
if clientAccount == "" || clientAccount != channelInfo.Founder() {
|
||||
csNotice(rb, client.t("You must be the channel founder to op"))
|
||||
return
|
||||
}
|
||||
@ -239,11 +135,6 @@ func csRegisterHandler(server *Server, client *Client, command, params string, r
|
||||
return
|
||||
}
|
||||
|
||||
if client.Account() == "" {
|
||||
csNotice(rb, client.t("You must be logged in to register a channel"))
|
||||
return
|
||||
}
|
||||
|
||||
// this provides the synchronization that allows exactly one registration of the channel:
|
||||
err = channelInfo.SetRegistered(client.Account())
|
||||
if err != nil {
|
||||
|
@ -46,7 +46,6 @@ type Client struct {
|
||||
capVersion caps.Version
|
||||
certfp string
|
||||
channels ChannelSet
|
||||
class *OperClass
|
||||
ctime time.Time
|
||||
exitedSnomaskSent bool
|
||||
fakelag *Fakelag
|
||||
@ -65,7 +64,7 @@ type Client struct {
|
||||
nickMaskCasefolded string
|
||||
nickMaskString string // cache for nickmask string since it's used with lots of replies
|
||||
nickTimer *NickTimer
|
||||
operName string
|
||||
oper *Oper
|
||||
preregNick string
|
||||
proxiedIP net.IP // actual remote IP if using the PROXY protocol
|
||||
quitMessage string
|
||||
@ -81,7 +80,6 @@ type Client struct {
|
||||
stateMutex sync.RWMutex // tier 1
|
||||
username string
|
||||
vhost string
|
||||
whoisLine string
|
||||
}
|
||||
|
||||
// NewClient returns a client with all the appropriate info setup.
|
||||
@ -312,10 +310,6 @@ func (client *Client) Ping() {
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// server goroutine
|
||||
//
|
||||
|
||||
// Register sets the client details as appropriate when entering the network.
|
||||
func (client *Client) Register() {
|
||||
client.stateMutex.Lock()
|
||||
@ -495,12 +489,13 @@ func (client *Client) HasUsername() bool {
|
||||
|
||||
// HasRoleCapabs returns true if client has the given (role) capabilities.
|
||||
func (client *Client) HasRoleCapabs(capabs ...string) bool {
|
||||
if client.class == nil {
|
||||
oper := client.Oper()
|
||||
if oper == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, capab := range capabs {
|
||||
if !client.class.Capabilities[capab] {
|
||||
if !oper.Class.Capabilities[capab] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -547,12 +542,45 @@ func (client *Client) Friends(capabs ...caps.Capability) ClientSet {
|
||||
return friends
|
||||
}
|
||||
|
||||
// XXX: CHGHOST requires prefix nickmask to have original hostname,
|
||||
// this is annoying to do correctly
|
||||
func (client *Client) sendChghost(oldNickMask string, vhost string) {
|
||||
username := client.Username()
|
||||
for fClient := range client.Friends(caps.ChgHost) {
|
||||
fClient.sendFromClientInternal("", client, oldNickMask, nil, "CHGHOST", username, vhost)
|
||||
}
|
||||
}
|
||||
|
||||
// choose the correct vhost to display
|
||||
func (client *Client) getVHostNoMutex() string {
|
||||
// hostserv vhost OR operclass vhost OR nothing (i.e., normal rdns hostmask)
|
||||
if client.vhost != "" {
|
||||
return client.vhost
|
||||
} else if client.oper != nil {
|
||||
return client.oper.Vhost
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// SetVHost updates the client's hostserv-based vhost
|
||||
func (client *Client) SetVHost(vhost string) (updated bool) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
updated = (client.vhost != vhost)
|
||||
client.vhost = vhost
|
||||
if updated {
|
||||
client.updateNickMaskNoMutex()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// updateNick updates `nick` and `nickCasefolded`.
|
||||
func (client *Client) updateNick(nick string) {
|
||||
casefoldedName, err := CasefoldName(nick)
|
||||
if err != nil {
|
||||
log.Println(fmt.Sprintf("ERROR: Nick [%s] couldn't be casefolded... this should never happen. Printing stacktrace.", client.nick))
|
||||
debug.PrintStack()
|
||||
client.server.logger.Error("internal", "nick couldn't be casefolded", nick, err.Error())
|
||||
return
|
||||
}
|
||||
client.stateMutex.Lock()
|
||||
client.nick = nick
|
||||
@ -573,19 +601,18 @@ func (client *Client) updateNickMask(nick string) {
|
||||
client.updateNickMaskNoMutex()
|
||||
}
|
||||
|
||||
// updateNickMask updates the casefolded nickname and nickmask, not holding any mutexes.
|
||||
// updateNickMask updates the casefolded nickname and nickmask, not acquiring any mutexes.
|
||||
func (client *Client) updateNickMaskNoMutex() {
|
||||
if len(client.vhost) > 0 {
|
||||
client.hostname = client.vhost
|
||||
} else {
|
||||
client.hostname = client.getVHostNoMutex()
|
||||
if client.hostname == "" {
|
||||
client.hostname = client.rawHostname
|
||||
}
|
||||
|
||||
nickMaskString := fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.hostname)
|
||||
nickMaskCasefolded, err := Casefold(nickMaskString)
|
||||
if err != nil {
|
||||
log.Println(fmt.Sprintf("ERROR: Nickmask [%s] couldn't be casefolded... this should never happen. Printing stacktrace.", client.nickMaskString))
|
||||
debug.PrintStack()
|
||||
client.server.logger.Error("internal", "nickmask couldn't be casefolded", nickMaskString, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
client.nickMaskString = nickMaskString
|
||||
@ -598,19 +625,26 @@ func (client *Client) AllNickmasks() []string {
|
||||
var mask string
|
||||
var err error
|
||||
|
||||
if len(client.vhost) > 0 {
|
||||
mask, err = Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.vhost))
|
||||
client.stateMutex.RLock()
|
||||
nick := client.nick
|
||||
username := client.username
|
||||
rawHostname := client.rawHostname
|
||||
vhost := client.getVHostNoMutex()
|
||||
client.stateMutex.RUnlock()
|
||||
|
||||
if len(vhost) > 0 {
|
||||
mask, err = Casefold(fmt.Sprintf("%s!%s@%s", nick, username, vhost))
|
||||
if err == nil {
|
||||
masks = append(masks, mask)
|
||||
}
|
||||
}
|
||||
|
||||
mask, err = Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.rawHostname))
|
||||
mask, err = Casefold(fmt.Sprintf("%s!%s@%s", nick, username, rawHostname))
|
||||
if err == nil {
|
||||
masks = append(masks, mask)
|
||||
}
|
||||
|
||||
mask2, err := Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.IPString()))
|
||||
mask2, err := Casefold(fmt.Sprintf("%s!%s@%s", nick, username, client.IPString()))
|
||||
if err == nil && mask2 != mask {
|
||||
masks = append(masks, mask2)
|
||||
}
|
||||
@ -772,6 +806,12 @@ func (client *Client) SendSplitMsgFromClient(msgid string, from *Client, tags *m
|
||||
// SendFromClient sends an IRC line coming from a specific client.
|
||||
// Adds account-tag to the line as well.
|
||||
func (client *Client) SendFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) error {
|
||||
return client.sendFromClientInternal(msgid, from, from.NickMaskString(), tags, command, params...)
|
||||
}
|
||||
|
||||
// XXX this is a hack where we allow overriding the client's nickmask
|
||||
// this is to support CHGHOST, which requires that we send the *original* nickmask with the response
|
||||
func (client *Client) sendFromClientInternal(msgid string, from *Client, nickmask string, tags *map[string]ircmsg.TagValue, command string, params ...string) error {
|
||||
// attach account-tag
|
||||
if client.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() {
|
||||
if tags == nil {
|
||||
@ -789,7 +829,7 @@ func (client *Client) SendFromClient(msgid string, from *Client, tags *map[strin
|
||||
}
|
||||
}
|
||||
|
||||
return client.Send(tags, from.nickMaskString, command, params...)
|
||||
return client.Send(tags, nickmask, command, params...)
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -92,14 +92,6 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"CHANSERV": {
|
||||
handler: csHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"CS": {
|
||||
handler: csHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"DEBUG": {
|
||||
handler: debugHandler,
|
||||
minParams: 1,
|
||||
@ -182,10 +174,6 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"NICKSERV": {
|
||||
handler: nsHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"NOTICE": {
|
||||
handler: noticeHandler,
|
||||
minParams: 2,
|
||||
@ -198,10 +186,6 @@ func init() {
|
||||
handler: npcaHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"NS": {
|
||||
handler: nsHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"OPER": {
|
||||
handler: operHandler,
|
||||
minParams: 2,
|
||||
@ -323,4 +307,6 @@ func init() {
|
||||
minParams: 1,
|
||||
},
|
||||
}
|
||||
|
||||
initializeServices()
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -64,6 +65,7 @@ type AccountConfig struct {
|
||||
AuthenticationEnabled bool `yaml:"authentication-enabled"`
|
||||
SkipServerPassword bool `yaml:"skip-server-password"`
|
||||
NickReservation NickReservationConfig `yaml:"nick-reservation"`
|
||||
VHosts VHostConfig
|
||||
}
|
||||
|
||||
// AccountRegistrationConfig controls account registration.
|
||||
@ -91,6 +93,18 @@ type AccountRegistrationConfig struct {
|
||||
AllowMultiplePerConnection bool `yaml:"allow-multiple-per-connection"`
|
||||
}
|
||||
|
||||
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 time.Duration
|
||||
} `yaml:"user-requests"`
|
||||
}
|
||||
|
||||
type NickReservationMethod int
|
||||
|
||||
const (
|
||||
@ -278,8 +292,8 @@ type OperClass struct {
|
||||
}
|
||||
|
||||
// OperatorClasses returns a map of assembled operator classes from the given config.
|
||||
func (conf *Config) OperatorClasses() (*map[string]OperClass, error) {
|
||||
ocs := make(map[string]OperClass)
|
||||
func (conf *Config) OperatorClasses() (map[string]*OperClass, error) {
|
||||
ocs := make(map[string]*OperClass)
|
||||
|
||||
// loop from no extends to most extended, breaking if we can't add any more
|
||||
lenOfLastOcs := -1
|
||||
@ -335,7 +349,7 @@ func (conf *Config) OperatorClasses() (*map[string]OperClass, error) {
|
||||
oc.WhoisLine += oc.Title
|
||||
}
|
||||
|
||||
ocs[name] = oc
|
||||
ocs[name] = &oc
|
||||
}
|
||||
|
||||
if !anyMissing {
|
||||
@ -344,11 +358,12 @@ func (conf *Config) OperatorClasses() (*map[string]OperClass, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return &ocs, nil
|
||||
return ocs, nil
|
||||
}
|
||||
|
||||
// Oper represents a single assembled operator's config.
|
||||
type Oper struct {
|
||||
Name string
|
||||
Class *OperClass
|
||||
WhoisLine string
|
||||
Vhost string
|
||||
@ -357,8 +372,8 @@ type Oper struct {
|
||||
}
|
||||
|
||||
// Operators returns a map of operator configs from the given OperClass and config.
|
||||
func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error) {
|
||||
operators := make(map[string]Oper)
|
||||
func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error) {
|
||||
operators := make(map[string]*Oper)
|
||||
for name, opConf := range conf.Opers {
|
||||
var oper Oper
|
||||
|
||||
@ -367,14 +382,15 @@ func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not casefold oper name: %s", err.Error())
|
||||
}
|
||||
oper.Name = name
|
||||
|
||||
oper.Pass = opConf.PasswordBytes()
|
||||
oper.Vhost = opConf.Vhost
|
||||
class, exists := (*oc)[opConf.Class]
|
||||
class, exists := oc[opConf.Class]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("Could not load operator [%s] - they use operclass [%s] which does not exist", name, opConf.Class)
|
||||
}
|
||||
oper.Class = &class
|
||||
oper.Class = class
|
||||
if len(opConf.WhoisLine) > 0 {
|
||||
oper.WhoisLine = opConf.WhoisLine
|
||||
} else {
|
||||
@ -388,7 +404,7 @@ func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error)
|
||||
oper.Modes = modeChanges
|
||||
|
||||
// successful, attach to list of opers
|
||||
operators[name] = oper
|
||||
operators[name] = &oper
|
||||
}
|
||||
return operators, nil
|
||||
}
|
||||
@ -537,6 +553,19 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
|
||||
if rawRegexp != "" {
|
||||
regexp, err := regexp.Compile(rawRegexp)
|
||||
if err == nil {
|
||||
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
|
||||
}
|
||||
|
||||
maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())
|
||||
|
@ -21,6 +21,7 @@ var (
|
||||
errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
|
||||
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
|
||||
errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname")
|
||||
errAccountUpdateFailed = errors.New("Error while updating your account information")
|
||||
errCallbackFailed = errors.New("Account verification could not be sent")
|
||||
errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
|
||||
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
||||
|
@ -75,9 +75,12 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (exiting bool)
|
||||
}
|
||||
|
||||
// given IP is sane! override the client's current IP
|
||||
rawHostname := utils.LookupHostname(proxiedIP)
|
||||
client.stateMutex.Lock()
|
||||
client.proxiedIP = parsedProxiedIP
|
||||
client.rawHostname = utils.LookupHostname(proxiedIP)
|
||||
client.hostname = client.rawHostname
|
||||
client.rawHostname = rawHostname
|
||||
client.stateMutex.Unlock()
|
||||
// nickmask will be updated when the client completes registration
|
||||
|
||||
// set tls info
|
||||
client.certfp = ""
|
||||
|
@ -83,6 +83,16 @@ func (server *Server) FakelagConfig() *FakelagConfig {
|
||||
return &server.config.Fakelag
|
||||
}
|
||||
|
||||
func (server *Server) GetOperator(name string) (oper *Oper) {
|
||||
name, err := CasefoldName(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
server.configurableStateMutex.RLock()
|
||||
defer server.configurableStateMutex.RUnlock()
|
||||
return server.operators[name]
|
||||
}
|
||||
|
||||
func (client *Client) Nick() string {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
@ -119,6 +129,18 @@ func (client *Client) Realname() string {
|
||||
return client.realname
|
||||
}
|
||||
|
||||
func (client *Client) Oper() *Oper {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.oper
|
||||
}
|
||||
|
||||
func (client *Client) SetOper(oper *Oper) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
client.oper = oper
|
||||
}
|
||||
|
||||
func (client *Client) Registered() bool {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/tidwall/buntdb"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// ACC [REGISTER|VERIFY] ...
|
||||
@ -494,12 +495,6 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
return false
|
||||
}
|
||||
|
||||
// CHANSERV [...]
|
||||
func csHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
server.chanservPrivmsgHandler(client, strings.Join(msg.Params, " "), rb)
|
||||
return false
|
||||
}
|
||||
|
||||
// DEBUG <subcmd>
|
||||
func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
param := strings.ToUpper(msg.Params[0])
|
||||
@ -562,7 +557,8 @@ func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
// DLINE LIST
|
||||
func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
// check oper permissions
|
||||
if !client.class.Capabilities["oper:local_ban"] {
|
||||
oper := client.Oper()
|
||||
if oper == nil || !oper.Class.Capabilities["oper:local_ban"] {
|
||||
rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
|
||||
return false
|
||||
}
|
||||
@ -665,7 +661,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
}
|
||||
}
|
||||
}
|
||||
operName := client.operName
|
||||
operName := oper.Name
|
||||
if operName == "" {
|
||||
operName = server.name
|
||||
}
|
||||
@ -977,7 +973,8 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
// KLINE LIST
|
||||
func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
// check oper permissions
|
||||
if !client.class.Capabilities["oper:local_ban"] {
|
||||
oper := client.Oper()
|
||||
if oper == nil || !oper.Class.Capabilities["oper:local_ban"] {
|
||||
rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
|
||||
return false
|
||||
}
|
||||
@ -1052,7 +1049,7 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
}
|
||||
|
||||
// get oper name
|
||||
operName := client.operName
|
||||
operName := oper.Name
|
||||
if operName == "" {
|
||||
operName = server.name
|
||||
}
|
||||
@ -1648,11 +1645,9 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if target == "chanserv" {
|
||||
server.chanservNoticeHandler(client, message, rb)
|
||||
continue
|
||||
} else if target == "nickserv" {
|
||||
server.nickservNoticeHandler(client, message, rb)
|
||||
|
||||
// NOTICEs sent to services are ignored
|
||||
if _, isService := OragonoServices[target]; isService {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -1715,46 +1710,29 @@ func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
return false
|
||||
}
|
||||
|
||||
// NICKSERV [params...]
|
||||
func nsHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
server.nickservPrivmsgHandler(client, strings.Join(msg.Params, " "), rb)
|
||||
return false
|
||||
}
|
||||
|
||||
// OPER <name> <password>
|
||||
func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
name, err := CasefoldName(msg.Params[0])
|
||||
if err != nil {
|
||||
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect"))
|
||||
return true
|
||||
}
|
||||
if client.HasMode(modes.Operator) == true {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, "OPER", client.t("You're already opered-up!"))
|
||||
return false
|
||||
}
|
||||
server.configurableStateMutex.RLock()
|
||||
oper := server.operators[name]
|
||||
server.configurableStateMutex.RUnlock()
|
||||
|
||||
password := []byte(msg.Params[1])
|
||||
err = passwd.ComparePassword(oper.Pass, password)
|
||||
if (oper.Pass == nil) || (err != nil) {
|
||||
authorized := false
|
||||
oper := server.GetOperator(msg.Params[0])
|
||||
if oper != nil {
|
||||
password := []byte(msg.Params[1])
|
||||
authorized = (bcrypt.CompareHashAndPassword(oper.Pass, password) == nil)
|
||||
}
|
||||
if !authorized {
|
||||
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect"))
|
||||
return true
|
||||
}
|
||||
|
||||
client.operName = name
|
||||
client.class = oper.Class
|
||||
client.whoisLine = oper.WhoisLine
|
||||
|
||||
// push new vhost if one is set
|
||||
if len(oper.Vhost) > 0 {
|
||||
for fClient := range client.Friends(caps.ChgHost) {
|
||||
fClient.SendFromClient("", client, nil, "CHGHOST", client.username, oper.Vhost)
|
||||
}
|
||||
// CHGHOST requires prefix nickmask to have original hostname, so do that before updating nickmask
|
||||
client.vhost = oper.Vhost
|
||||
client.updateNickMask("")
|
||||
oldNickmask := client.NickMaskString()
|
||||
client.SetOper(oper)
|
||||
client.updateNickMask("")
|
||||
if client.NickMaskString() != oldNickmask {
|
||||
client.sendChghost(oldNickmask, oper.Vhost)
|
||||
}
|
||||
|
||||
// set new modes: modes.Operator, plus anything specified in the config
|
||||
@ -1769,7 +1747,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
rb.Add(nil, server.name, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator"))
|
||||
rb.Add(nil, server.name, "MODE", client.nick, applied.String())
|
||||
|
||||
server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, client.operName))
|
||||
server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name))
|
||||
|
||||
// client may now be unthrottled by the fakelag system
|
||||
client.resetFakelag()
|
||||
@ -1868,11 +1846,8 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg, rb)
|
||||
} else {
|
||||
target, err = CasefoldName(targetString)
|
||||
if target == "chanserv" {
|
||||
server.chanservPrivmsgHandler(client, message, rb)
|
||||
continue
|
||||
} else if target == "nickserv" {
|
||||
server.nickservPrivmsgHandler(client, message, rb)
|
||||
if service, isService := OragonoServices[target]; isService {
|
||||
servicePrivmsgHandler(service, server, client, message, rb)
|
||||
continue
|
||||
}
|
||||
user := server.clients.Get(target)
|
||||
@ -2179,7 +2154,8 @@ func topicHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
// UNDLINE <ip>|<net>
|
||||
func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
// check oper permissions
|
||||
if !client.class.Capabilities["oper:local_unban"] {
|
||||
oper := client.Oper()
|
||||
if oper == nil || !oper.Class.Capabilities["oper:local_unban"] {
|
||||
rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
|
||||
return false
|
||||
}
|
||||
@ -2242,7 +2218,8 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
// UNKLINE <mask>
|
||||
func unKLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
// check oper permissions
|
||||
if !client.class.Capabilities["oper:local_unban"] {
|
||||
oper := client.Oper()
|
||||
if oper == nil || !oper.Class.Capabilities["oper:local_unban"] {
|
||||
rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
|
||||
return false
|
||||
}
|
||||
|
12
irc/help.go
12
irc/help.go
@ -187,6 +187,18 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
|
||||
text: `HELPOP <argument>
|
||||
|
||||
Get an explanation of <argument>, or "index" for a list of help topics.`,
|
||||
},
|
||||
"hostserv": {
|
||||
text: `HOSTSERV <command> [params]
|
||||
|
||||
HostServ lets you manage your vhost (a string displayed in place of your
|
||||
real hostname).`,
|
||||
},
|
||||
"hs": {
|
||||
text: `HS <command> [params]
|
||||
|
||||
HostServ lets you manage your vhost (a string displayed in place of your
|
||||
real hostname).`,
|
||||
},
|
||||
"info": {
|
||||
text: `INFO
|
||||
|
314
irc/hostserv.go
Normal file
314
irc/hostserv.go
Normal file
@ -0,0 +1,314 @@
|
||||
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
const hostservHelp = `HostServ lets you manage your vhost (i.e., the string displayed
|
||||
in place of your client's hostname/IP).
|
||||
|
||||
To see in-depth help for a specific HostServ command, try:
|
||||
$b/HS HELP <command>$b
|
||||
|
||||
Here are the commands you can use:
|
||||
%s`
|
||||
|
||||
var (
|
||||
errVHostBadCharacters = errors.New("Vhost contains prohibited characters")
|
||||
errVHostTooLong = errors.New("Vhost is too long")
|
||||
// ascii only for now
|
||||
defaultValidVhostRegex = regexp.MustCompile(`^[0-9A-Za-z.\-_/]+$`)
|
||||
)
|
||||
|
||||
func hostservEnabled(server *Server) bool {
|
||||
return server.AccountConfig().VHosts.Enabled
|
||||
}
|
||||
|
||||
func hostservRequestsEnabled(server *Server) bool {
|
||||
ac := server.AccountConfig()
|
||||
return ac.VHosts.Enabled && ac.VHosts.UserRequests.Enabled
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
"status": {
|
||||
handler: hsStatusHandler,
|
||||
help: `Syntax: $bSTATUS$b
|
||||
|
||||
STATUS displays your current vhost, if any, and the status of your most recent
|
||||
request for a new one.`,
|
||||
helpShort: `$bSTATUS$b shows your vhost and request status.`,
|
||||
authRequired: true,
|
||||
enabled: hostservEnabled,
|
||||
},
|
||||
"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.`,
|
||||
capabs: []string{"vhosts"},
|
||||
enabled: hostservEnabled,
|
||||
},
|
||||
"del": {
|
||||
handler: hsSetHandler,
|
||||
help: `Syntax: $bDEL <user>$b
|
||||
|
||||
DEL sets a user's vhost, bypassing the request system.`,
|
||||
helpShort: `$bDEL$b deletes a user's vhost.`,
|
||||
capabs: []string{"vhosts"},
|
||||
enabled: hostservEnabled,
|
||||
},
|
||||
"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,
|
||||
},
|
||||
"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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// hsNotice sends the client a notice from HostServ
|
||||
func hsNotice(rb *ResponseBuffer, text string) {
|
||||
rb.Add(nil, "HostServ", "NOTICE", rb.target.Nick(), text)
|
||||
}
|
||||
|
||||
// hsNotifyChannel notifies the designated channel of new vhost activity
|
||||
func hsNotifyChannel(server *Server, message string) {
|
||||
chname := server.AccountConfig().VHosts.UserRequests.Channel
|
||||
channel := server.channels.Get(chname)
|
||||
if channel == nil {
|
||||
return
|
||||
}
|
||||
chname = channel.Name()
|
||||
for _, client := range channel.Members() {
|
||||
client.Send(nil, "HostServ", "PRIVMSG", chname, message)
|
||||
}
|
||||
}
|
||||
|
||||
func hsOnOffHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
enable := false
|
||||
if command == "on" {
|
||||
enable = true
|
||||
}
|
||||
|
||||
_, err := server.accounts.VHostSetEnabled(client, enable)
|
||||
if err != nil {
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
func hsRequestHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
vhost, _ := utils.ExtractParam(params)
|
||||
if validateVhost(server, vhost, false) != nil {
|
||||
hsNotice(rb, client.t("Invalid vhost"))
|
||||
return
|
||||
}
|
||||
|
||||
accountName := client.Account()
|
||||
account, err := server.accounts.LoadAccount(client.Account())
|
||||
if err != nil {
|
||||
hsNotice(rb, client.t("An error occurred"))
|
||||
return
|
||||
}
|
||||
elapsed := time.Now().Sub(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"))
|
||||
} else {
|
||||
hsNotice(rb, fmt.Sprintf(client.t("Your vhost request will be reviewed by an administrator")))
|
||||
chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost)
|
||||
hsNotifyChannel(server, chanMsg)
|
||||
// TODO send admins a snomask of some kind
|
||||
}
|
||||
}
|
||||
|
||||
func hsStatusHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
accountName := client.Account()
|
||||
account, err := server.accounts.LoadAccount(accountName)
|
||||
if err != nil {
|
||||
server.logger.Warning("internal", "error loading account info", accountName, err.Error())
|
||||
hsNotice(rb, client.t("An error occurred"))
|
||||
return
|
||||
}
|
||||
|
||||
if account.VHost.ApprovedVHost != "" {
|
||||
hsNotice(rb, fmt.Sprintf(client.t("Account %s has vhost: %s"), accountName, account.VHost.ApprovedVHost))
|
||||
if !account.VHost.Enabled {
|
||||
hsNotice(rb, fmt.Sprintf(client.t("This vhost is currently disabled, but can be enabled with /HS ON")))
|
||||
}
|
||||
} 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 {
|
||||
ac := server.AccountConfig()
|
||||
if len(vhost) > ac.VHosts.MaxLength {
|
||||
return errVHostTooLong
|
||||
}
|
||||
if !ac.VHosts.ValidRegexp.MatchString(vhost) {
|
||||
return errVHostBadCharacters
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hsSetHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
var user, vhost string
|
||||
user, params = utils.ExtractParam(params)
|
||||
if user == "" {
|
||||
hsNotice(rb, client.t("A user is required"))
|
||||
return
|
||||
}
|
||||
if command == "set" {
|
||||
vhost, _ = utils.ExtractParam(params)
|
||||
if validateVhost(server, vhost, true) != nil {
|
||||
hsNotice(rb, client.t("Invalid vhost"))
|
||||
return
|
||||
}
|
||||
} else if command != "del" {
|
||||
server.logger.Warning("internal", "invalid hostserv set command", command)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := server.accounts.VHostSet(user, 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"))
|
||||
}
|
||||
}
|
||||
|
||||
func hsWaitingHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
requests, total := server.accounts.VHostListRequests(10)
|
||||
hsNotice(rb, fmt.Sprintf(client.t("There are %d pending requests for vhosts (%d displayed)"), total, len(requests)))
|
||||
for i, request := range requests {
|
||||
hsNotice(rb, fmt.Sprintf(client.t("%d. User %s requests vhost: %s"), i+1, request.Account, request.RequestedVHost))
|
||||
}
|
||||
}
|
||||
|
||||
func hsApproveHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
user, _ := utils.ExtractParam(params)
|
||||
if user == "" {
|
||||
hsNotice(rb, client.t("A user is required"))
|
||||
return
|
||||
}
|
||||
|
||||
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 %s approved vhost %s for account %s", client.Nick(), vhostInfo.ApprovedVHost, user)
|
||||
hsNotifyChannel(server, chanMsg)
|
||||
for _, client := range server.accounts.AccountToClients(user) {
|
||||
client.Notice(client.t("Your vhost request was approved by an administrator"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hsRejectHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
user, params := utils.ExtractParam(params)
|
||||
if user == "" {
|
||||
hsNotice(rb, client.t("A user is required"))
|
||||
return
|
||||
}
|
||||
reason := strings.TrimSpace(params)
|
||||
|
||||
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)
|
||||
for _, client := range server.accounts.AccountToClients(user) {
|
||||
if reason == "" {
|
||||
client.Notice("Your vhost request was rejected by an administrator")
|
||||
} else {
|
||||
client.Notice(fmt.Sprintf(client.t("Your vhost request was rejected by an administrator. The reason given was: %s"), reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -16,10 +16,7 @@ import (
|
||||
|
||||
var (
|
||||
restrictedNicknames = map[string]bool{
|
||||
"=scene=": true, // used for rp commands
|
||||
"chanserv": true,
|
||||
"nickserv": true,
|
||||
"hostserv": true,
|
||||
"=scene=": true, // used for rp commands
|
||||
}
|
||||
)
|
||||
|
||||
|
157
irc/nickserv.go
157
irc/nickserv.go
@ -5,15 +5,20 @@ package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
// "enabled" callbacks for specific nickserv commands
|
||||
func servCmdRequiresAccreg(server *Server) bool {
|
||||
return server.AccountConfig().Registration.Enabled
|
||||
}
|
||||
|
||||
func servCmdRequiresAuthEnabled(server *Server) bool {
|
||||
return server.AccountConfig().AuthenticationEnabled
|
||||
}
|
||||
|
||||
const nickservHelp = `NickServ lets you register and login to an account.
|
||||
|
||||
To see in-depth help for a specific NickServ command, try:
|
||||
@ -22,24 +27,16 @@ To see in-depth help for a specific NickServ command, try:
|
||||
Here are the commands you can use:
|
||||
%s`
|
||||
|
||||
type nsCommand struct {
|
||||
capabs []string // oper capabs the given user has to have to access this command
|
||||
handler func(server *Server, client *Client, command, params string, rb *ResponseBuffer)
|
||||
help string
|
||||
helpShort string
|
||||
nickReservation bool // nick reservation must be enabled to use this command
|
||||
oper bool // true if the user has to be an oper to use this command
|
||||
}
|
||||
|
||||
var (
|
||||
nickservCommands = map[string]*nsCommand{
|
||||
nickservCommands = map[string]*serviceCommand{
|
||||
"drop": {
|
||||
handler: nsDropHandler,
|
||||
help: `Syntax: $bDROP [nickname]$b
|
||||
|
||||
DROP de-links the given (or your current) nickname from your user account.`,
|
||||
helpShort: `$bDROP$b de-links your current (or the given) nickname from your user account.`,
|
||||
nickReservation: true,
|
||||
helpShort: `$bDROP$b de-links your current (or the given) nickname from your user account.`,
|
||||
enabled: servCmdRequiresAccreg,
|
||||
authRequired: true,
|
||||
},
|
||||
"ghost": {
|
||||
handler: nsGhostHandler,
|
||||
@ -47,7 +44,8 @@ DROP de-links the given (or your current) nickname from your user account.`,
|
||||
|
||||
GHOST disconnects the given user from the network if they're logged in with the
|
||||
same user account, letting you reclaim your nickname.`,
|
||||
helpShort: `$bGHOST$b reclaims your nickname.`,
|
||||
helpShort: `$bGHOST$b reclaims your nickname.`,
|
||||
authRequired: true,
|
||||
},
|
||||
"group": {
|
||||
handler: nsGroupHandler,
|
||||
@ -55,15 +53,11 @@ same user account, letting you reclaim your nickname.`,
|
||||
|
||||
GROUP links your current nickname with your logged-in account, preventing other
|
||||
users from changing to it (or forcing them to rename).`,
|
||||
helpShort: `$bGROUP$b links your current nickname to your user account.`,
|
||||
nickReservation: true,
|
||||
helpShort: `$bGROUP$b links your current nickname to your user account.`,
|
||||
enabled: servCmdRequiresAccreg,
|
||||
authRequired: true,
|
||||
},
|
||||
"help": {
|
||||
help: `Syntax: $bHELP [command]$b
|
||||
|
||||
HELP returns information on the given command.`,
|
||||
helpShort: `$bHELP$b shows in-depth information about commands.`,
|
||||
},
|
||||
"identify": {
|
||||
handler: nsIdentifyHandler,
|
||||
help: `Syntax: $bIDENTIFY <username> [password]$b
|
||||
@ -91,15 +85,16 @@ registration, you can send an asterisk (*) as the email address.
|
||||
If the password is left out, your account will be registered to your TLS client
|
||||
certificate (and you will need to use that certificate to login in future).`,
|
||||
helpShort: `$bREGISTER$b lets you register a user account.`,
|
||||
enabled: servCmdRequiresAccreg,
|
||||
},
|
||||
"sadrop": {
|
||||
handler: nsDropHandler,
|
||||
help: `Syntax: $bSADROP <nickname>$b
|
||||
|
||||
SADROP foribly de-links the given nickname from the attached user account.`,
|
||||
helpShort: `$bSADROP$b forcibly de-links the given nickname from its user account.`,
|
||||
nickReservation: true,
|
||||
capabs: []string{"unregister"},
|
||||
SADROP forcibly de-links the given nickname from the attached user account.`,
|
||||
helpShort: `$bSADROP$b forcibly de-links the given nickname from its user account.`,
|
||||
capabs: []string{"unregister"},
|
||||
enabled: servCmdRequiresAccreg,
|
||||
},
|
||||
"unregister": {
|
||||
handler: nsUnregisterHandler,
|
||||
@ -116,6 +111,7 @@ IRC operator with the correct permissions).`,
|
||||
VERIFY lets you complete an account registration, if the server requires email
|
||||
or other verification.`,
|
||||
helpShort: `$bVERIFY$b lets you complete account registration.`,
|
||||
enabled: servCmdRequiresAccreg,
|
||||
},
|
||||
}
|
||||
)
|
||||
@ -125,53 +121,6 @@ func nsNotice(rb *ResponseBuffer, text string) {
|
||||
rb.Add(nil, "NickServ", "NOTICE", rb.target.Nick(), text)
|
||||
}
|
||||
|
||||
// nickservNoticeHandler handles NOTICEs that NickServ receives.
|
||||
func (server *Server) nickservNoticeHandler(client *Client, message string, rb *ResponseBuffer) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// nickservPrivmsgHandler handles PRIVMSGs that NickServ receives.
|
||||
func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb *ResponseBuffer) {
|
||||
commandName, params := utils.ExtractParam(message)
|
||||
commandName = strings.ToLower(commandName)
|
||||
|
||||
commandInfo := nickservCommands[commandName]
|
||||
if commandInfo == nil {
|
||||
nsNotice(rb, client.t("Unknown command. To see available commands, run /NS HELP"))
|
||||
return
|
||||
}
|
||||
|
||||
if commandInfo.oper && !client.HasMode(modes.Operator) {
|
||||
nsNotice(rb, client.t("Command restricted"))
|
||||
return
|
||||
}
|
||||
|
||||
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
|
||||
nsNotice(rb, client.t("Command restricted"))
|
||||
return
|
||||
}
|
||||
|
||||
if commandInfo.nickReservation && !server.AccountConfig().Registration.Enabled {
|
||||
nsNotice(rb, client.t("Account registration has been disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
// custom help handling here to prevent recursive init loop
|
||||
if commandName == "help" {
|
||||
nsHelpHandler(server, client, commandName, params, rb)
|
||||
return
|
||||
}
|
||||
|
||||
if commandInfo.handler == nil {
|
||||
nsNotice(rb, client.t("Command error. Please report this to the developers"))
|
||||
return
|
||||
}
|
||||
|
||||
server.logger.Debug("nickserv", fmt.Sprintf("Client %s ran command %s", client.Nick(), commandName))
|
||||
|
||||
commandInfo.handler(server, client, commandName, params, rb)
|
||||
}
|
||||
|
||||
func nsDropHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
sadrop := command == "sadrop"
|
||||
nick, _ := utils.ExtractParam(params)
|
||||
@ -218,12 +167,6 @@ func nsGhostHandler(server *Server, client *Client, command, params string, rb *
|
||||
}
|
||||
|
||||
func nsGroupHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
account := client.Account()
|
||||
if account == "" {
|
||||
nsNotice(rb, client.t("You're not logged into an account"))
|
||||
return
|
||||
}
|
||||
|
||||
nick := client.NickCasefolded()
|
||||
err := server.accounts.SetNickReserved(client, nick, false, true)
|
||||
if err == nil {
|
||||
@ -237,59 +180,7 @@ func nsGroupHandler(server *Server, client *Client, command, params string, rb *
|
||||
}
|
||||
}
|
||||
|
||||
func nsHelpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
nsNotice(rb, ircfmt.Unescape(client.t("*** $bNickServ HELP$b ***")))
|
||||
|
||||
if params == "" {
|
||||
// show general help
|
||||
var shownHelpLines sort.StringSlice
|
||||
for _, commandInfo := range nickservCommands {
|
||||
// skip commands user can't access
|
||||
if commandInfo.oper && !client.HasMode(modes.Operator) {
|
||||
continue
|
||||
}
|
||||
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
|
||||
continue
|
||||
}
|
||||
if commandInfo.nickReservation && !server.AccountConfig().Registration.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
shownHelpLines = append(shownHelpLines, " "+client.t(commandInfo.helpShort))
|
||||
}
|
||||
|
||||
// sort help lines
|
||||
sort.Sort(shownHelpLines)
|
||||
|
||||
// assemble help text
|
||||
assembledHelpLines := strings.Join(shownHelpLines, "\n")
|
||||
fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(nickservHelp), assembledHelpLines))
|
||||
|
||||
// push out help text
|
||||
for _, line := range strings.Split(fullHelp, "\n") {
|
||||
nsNotice(rb, line)
|
||||
}
|
||||
} else {
|
||||
commandInfo := nickservCommands[strings.ToLower(strings.TrimSpace(params))]
|
||||
if commandInfo == nil {
|
||||
nsNotice(rb, client.t("Unknown command. To see available commands, run /NS HELP"))
|
||||
} else {
|
||||
for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
|
||||
nsNotice(rb, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nsNotice(rb, ircfmt.Unescape(client.t("*** $bEnd of NickServ HELP$b ***")))
|
||||
}
|
||||
|
||||
func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||
// fail out if we need to
|
||||
if !server.AccountConfig().AuthenticationEnabled {
|
||||
nsNotice(rb, client.t("Login has been disabled"))
|
||||
return
|
||||
}
|
||||
|
||||
loginSuccessful := false
|
||||
|
||||
username, passphrase := utils.ExtractParam(params)
|
||||
|
@ -115,8 +115,8 @@ type Server struct {
|
||||
name string
|
||||
nameCasefolded string
|
||||
networkName string
|
||||
operators map[string]Oper
|
||||
operclasses map[string]OperClass
|
||||
operators map[string]*Oper
|
||||
operclasses map[string]*OperClass
|
||||
password []byte
|
||||
passwords *passwd.SaltedManager
|
||||
recoverFromErrors bool
|
||||
@ -659,8 +659,9 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
||||
if whoischannels != nil {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " "))
|
||||
}
|
||||
if target.class != nil {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine)
|
||||
tOper := target.Oper()
|
||||
if tOper != nil {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, tOper.WhoisLine)
|
||||
}
|
||||
if client.HasMode(modes.Operator) || client == target {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, client.nick, target.nick, fmt.Sprintf("%s@%s", target.username, utils.LookupHostname(target.IPString())), target.IPString(), client.t("Actual user@host, Actual IP"))
|
||||
@ -863,6 +864,12 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
||||
server.accounts.buildNickToAccountIndex()
|
||||
}
|
||||
|
||||
hsPreviouslyDisabled := oldAccountConfig != nil && !oldAccountConfig.VHosts.Enabled
|
||||
hsNowEnabled := config.Accounts.VHosts.Enabled
|
||||
if hsPreviouslyDisabled && hsNowEnabled {
|
||||
server.accounts.initVHostRequestQueue()
|
||||
}
|
||||
|
||||
// STS
|
||||
stsValue := config.Server.STS.Value()
|
||||
var stsDisabled bool
|
||||
@ -944,7 +951,7 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
||||
ChanListModes: int(config.Limits.ChanListModes),
|
||||
LineLen: lineLenConfig,
|
||||
}
|
||||
server.operclasses = *operclasses
|
||||
server.operclasses = operclasses
|
||||
server.operators = opers
|
||||
server.checkIdent = config.Server.CheckIdent
|
||||
|
||||
|
194
irc/services.go
Normal file
194
irc/services.go
Normal file
@ -0,0 +1,194 @@
|
||||
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
// defines an IRC service, e.g., NICKSERV
|
||||
type ircService struct {
|
||||
Name string
|
||||
ShortName string
|
||||
CommandAliases []string
|
||||
Commands map[string]*serviceCommand
|
||||
HelpBanner string
|
||||
}
|
||||
|
||||
// defines a command associated with a service, e.g., NICKSERV IDENTIFY
|
||||
type serviceCommand struct {
|
||||
capabs []string // oper capabs the given user has to have to access this command
|
||||
handler func(server *Server, client *Client, command, params string, rb *ResponseBuffer)
|
||||
help string
|
||||
helpShort string
|
||||
authRequired bool
|
||||
enabled func(*Server) bool // is this command enabled in the server config?
|
||||
}
|
||||
|
||||
// all services, by lowercase name
|
||||
var OragonoServices = map[string]*ircService{
|
||||
"nickserv": {
|
||||
Name: "NickServ",
|
||||
ShortName: "NS",
|
||||
CommandAliases: []string{"NICKSERV", "NS"},
|
||||
Commands: nickservCommands,
|
||||
HelpBanner: nickservHelp,
|
||||
},
|
||||
"chanserv": {
|
||||
Name: "ChanServ",
|
||||
ShortName: "CS",
|
||||
CommandAliases: []string{"CHANSERV", "CS"},
|
||||
Commands: chanservCommands,
|
||||
HelpBanner: chanservHelp,
|
||||
},
|
||||
"hostserv": {
|
||||
Name: "HostServ",
|
||||
ShortName: "HS",
|
||||
CommandAliases: []string{"HOSTSERV", "HS"},
|
||||
Commands: hostservCommands,
|
||||
HelpBanner: hostservHelp,
|
||||
},
|
||||
}
|
||||
|
||||
// all service commands at the protocol level, by uppercase command name
|
||||
// e.g., NICKSERV, NS
|
||||
var oragonoServicesByCommandAlias map[string]*ircService
|
||||
|
||||
// special-cased command shared by all services
|
||||
var servHelpCmd serviceCommand = serviceCommand{
|
||||
help: `Syntax: $bHELP [command]$b
|
||||
|
||||
HELP returns information on the given command.`,
|
||||
helpShort: `$bHELP$b shows in-depth information about commands.`,
|
||||
}
|
||||
|
||||
// this handles IRC commands like `/NICKSERV INFO`, translating into `/MSG NICKSERV INFO`
|
||||
func serviceCmdHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
service, ok := oragonoServicesByCommandAlias[msg.Command]
|
||||
if !ok {
|
||||
server.logger.Warning("internal", "can't handle unrecognized service", msg.Command)
|
||||
return false
|
||||
}
|
||||
|
||||
fakePrivmsg := strings.Join(msg.Params, " ")
|
||||
servicePrivmsgHandler(service, server, client, fakePrivmsg, rb)
|
||||
return false
|
||||
}
|
||||
|
||||
// generic handler for service PRIVMSG
|
||||
func servicePrivmsgHandler(service *ircService, server *Server, client *Client, message string, rb *ResponseBuffer) {
|
||||
commandName, params := utils.ExtractParam(message)
|
||||
commandName = strings.ToLower(commandName)
|
||||
|
||||
nick := rb.target.Nick()
|
||||
sendNotice := func(notice string) {
|
||||
rb.Add(nil, service.Name, "NOTICE", nick, notice)
|
||||
}
|
||||
|
||||
cmd := service.Commands[commandName]
|
||||
if cmd == nil {
|
||||
sendNotice(fmt.Sprintf("%s /%s HELP", client.t("Unknown command. To see available commands, run"), service.ShortName))
|
||||
return
|
||||
}
|
||||
|
||||
if cmd.enabled != nil && !cmd.enabled(server) {
|
||||
sendNotice(client.t("This command has been disabled by the server administrators"))
|
||||
return
|
||||
}
|
||||
|
||||
if 0 < len(cmd.capabs) && !client.HasRoleCapabs(cmd.capabs...) {
|
||||
sendNotice(client.t("Command restricted"))
|
||||
return
|
||||
}
|
||||
|
||||
if cmd.authRequired && client.Account() == "" {
|
||||
sendNotice(client.t("You're not logged into an account"))
|
||||
return
|
||||
}
|
||||
|
||||
server.logger.Debug("services", fmt.Sprintf("Client %s ran %s command %s", client.Nick(), service.Name, commandName))
|
||||
if commandName == "help" {
|
||||
serviceHelpHandler(service, server, client, params, rb)
|
||||
} else {
|
||||
cmd.handler(server, client, commandName, params, rb)
|
||||
}
|
||||
}
|
||||
|
||||
// generic handler that displays help for service commands
|
||||
func serviceHelpHandler(service *ircService, server *Server, client *Client, params string, rb *ResponseBuffer) {
|
||||
nick := rb.target.Nick()
|
||||
sendNotice := func(notice string) {
|
||||
rb.Add(nil, service.Name, "NOTICE", nick, notice)
|
||||
}
|
||||
|
||||
sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
|
||||
|
||||
if params == "" {
|
||||
// show general help
|
||||
var shownHelpLines sort.StringSlice
|
||||
for _, commandInfo := range service.Commands {
|
||||
// skip commands user can't access
|
||||
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
|
||||
continue
|
||||
}
|
||||
if commandInfo.enabled != nil && !commandInfo.enabled(server) {
|
||||
continue
|
||||
}
|
||||
|
||||
shownHelpLines = append(shownHelpLines, " "+client.t(commandInfo.helpShort))
|
||||
}
|
||||
|
||||
// sort help lines
|
||||
sort.Sort(shownHelpLines)
|
||||
|
||||
// assemble help text
|
||||
assembledHelpLines := strings.Join(shownHelpLines, "\n")
|
||||
fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(service.HelpBanner), assembledHelpLines))
|
||||
|
||||
// push out help text
|
||||
for _, line := range strings.Split(fullHelp, "\n") {
|
||||
sendNotice(line)
|
||||
}
|
||||
} else {
|
||||
commandInfo := service.Commands[strings.ToLower(strings.TrimSpace(params))]
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendNotice(ircfmt.Unescape(fmt.Sprintf(client.t("*** $bEnd of %s HELP$b ***"), service.Name)))
|
||||
}
|
||||
|
||||
func initializeServices() {
|
||||
// this modifies the global Commands map,
|
||||
// so it must be called from irc/commands.go's init()
|
||||
oragonoServicesByCommandAlias = make(map[string]*ircService)
|
||||
|
||||
for serviceName, service := range OragonoServices {
|
||||
// make `/MSG ServiceName HELP` work correctly
|
||||
service.Commands["help"] = &servHelpCmd
|
||||
|
||||
// reserve the nickname
|
||||
restrictedNicknames[serviceName] = true
|
||||
|
||||
// register the protocol-level commands (NICKSERV, NS) that talk to the service
|
||||
var ircCmdDef Command
|
||||
ircCmdDef.handler = serviceCmdHandler
|
||||
for _, ircCmd := range service.CommandAliases {
|
||||
Commands[ircCmd] = ircCmdDef
|
||||
oragonoServicesByCommandAlias[ircCmd] = service
|
||||
}
|
||||
}
|
||||
}
|
31
oragono.yaml
31
oragono.yaml
@ -198,6 +198,36 @@ accounts:
|
||||
# rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
|
||||
rename-prefix: Guest-
|
||||
|
||||
# vhosts controls the assignment of vhosts (strings displayed in place of the user's
|
||||
# hostname/IP) by the HostServ service
|
||||
vhosts:
|
||||
# are vhosts enabled at all?
|
||||
enabled: true
|
||||
|
||||
# maximum length of a vhost
|
||||
max-length: 64
|
||||
|
||||
# regexp for testing the validity of a vhost
|
||||
# (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
|
||||
|
||||
# channel options
|
||||
channels:
|
||||
# modes that are set when new channels are created
|
||||
@ -252,6 +282,7 @@ oper-classes:
|
||||
- "oper:die"
|
||||
- "unregister"
|
||||
- "samode"
|
||||
- "vhosts"
|
||||
|
||||
# ircd operators
|
||||
opers:
|
||||
|
Loading…
Reference in New Issue
Block a user