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

Merge pull request #247 from slingamn/vhosts.3

initial vhosts implementation, #183
This commit is contained in:
Daniel Oaks 2018-05-19 08:51:16 +10:00 committed by GitHub
commit de7b679fc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1069 additions and 375 deletions

View File

@ -14,6 +14,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/caps"
@ -30,14 +31,24 @@ const (
keyAccountRegTime = "account.registered.time %s" keyAccountRegTime = "account.registered.time %s"
keyAccountCredentials = "account.credentials %s" keyAccountCredentials = "account.credentials %s"
keyAccountAdditionalNicks = "account.additionalnicks %s" keyAccountAdditionalNicks = "account.additionalnicks %s"
keyAccountVHost = "account.vhost %s"
keyCertToAccount = "account.creds.certfp %s" keyCertToAccount = "account.creds.certfp %s"
keyVHostQueueAcctToId = "vhostQueue %s"
vhostRequestIdx = "vhostQueue"
) )
// everything about accounts is persistent; therefore, the database is the authoritative // 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 // source of truth for all account information. anything on the heap is just a cache
type AccountManager struct { 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 sync.RWMutex // tier 2
serialCacheUpdateMutex sync.Mutex // tier 3 serialCacheUpdateMutex sync.Mutex // tier 3
vHostUpdateMutex sync.Mutex // tier 3
server *Server server *Server
// track clients logged in to accounts // track clients logged in to accounts
@ -53,6 +64,7 @@ func NewAccountManager(server *Server) *AccountManager {
} }
am.buildNickToAccountIndex() am.buildNickToAccountIndex()
am.initVHostRequestQueue()
return &am return &am
} }
@ -94,10 +106,46 @@ func (am *AccountManager) buildNickToAccountIndex() {
am.nickToAccount = result am.nickToAccount = result
am.Unlock() am.Unlock()
} }
}
func (am *AccountManager) initVHostRequestQueue() {
if !am.server.AccountConfig().VHosts.Enabled {
return 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 { func (am *AccountManager) NickToAccount(nick string) string {
cfnick, err := CasefoldName(nick) cfnick, err := CasefoldName(nick)
if err != nil { if err != nil {
@ -109,6 +157,17 @@ func (am *AccountManager) NickToAccount(nick string) string {
return am.nickToAccount[cfnick] 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 { func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
casefoldedAccount, err := CasefoldName(account) casefoldedAccount, err := CasefoldName(account)
if err != nil || account == "" || account == "*" { if err != nil || account == "" || account == "*" {
@ -342,7 +401,12 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
return err 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 return nil
} }
@ -464,7 +528,7 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
return errAccountInvalidCredentials return errAccountInvalidCredentials
} }
am.Login(client, account.Name) am.Login(client, account)
return nil return nil
} }
@ -484,6 +548,11 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
return return
} }
result, err = am.deserializeRawAccount(raw)
return
}
func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result ClientAccount, err error) {
result.Name = raw.Name result.Name = raw.Name
regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64) regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
result.RegisteredAt = time.Unix(regTimeInt, 0) result.RegisteredAt = time.Unix(regTimeInt, 0)
@ -495,6 +564,13 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
} }
result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks) result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
result.Verified = raw.Verified 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 return
} }
@ -506,6 +582,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount) callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
_, e := tx.Get(accountKey) _, e := tx.Get(accountKey)
if e == buntdb.ErrNotFound { if e == buntdb.ErrNotFound {
@ -518,6 +595,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
result.Credentials, _ = tx.Get(credentialsKey) result.Credentials, _ = tx.Get(credentialsKey)
result.Callback, _ = tx.Get(callbackKey) result.Callback, _ = tx.Get(callbackKey)
result.AdditionalNicks, _ = tx.Get(nicksKey) result.AdditionalNicks, _ = tx.Get(nicksKey)
result.VHost, _ = tx.Get(vhostKey)
if _, e = tx.Get(verifiedKey); e == nil { if _, e = tx.Get(verifiedKey); e == nil {
result.Verified = true result.Verified = true
@ -540,6 +618,8 @@ func (am *AccountManager) Unregister(account string) error {
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount) verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
var clients []*Client var clients []*Client
@ -560,6 +640,9 @@ func (am *AccountManager) Unregister(account string) error {
tx.Delete(nicksKey) tx.Delete(nicksKey)
credText, err = tx.Get(credentialsKey) credText, err = tx.Get(credentialsKey)
tx.Delete(credentialsKey) tx.Delete(credentialsKey)
tx.Delete(vhostKey)
_, err := tx.Delete(vhostQueueKey)
am.decrementVHostQueueCount(casefoldedAccount, err)
return nil return nil
}) })
@ -624,17 +707,239 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
} }
// ok, we found an account corresponding to their certificate // ok, we found an account corresponding to their certificate
clientAccount, err := am.deserializeRawAccount(rawAccount)
am.Login(client, rawAccount.Name) if err != nil {
return err
}
am.Login(client, clientAccount)
return nil 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() am.Lock()
defer am.Unlock() defer am.Unlock()
am.loginToAccount(client, account)
casefoldedAccount := client.Account()
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client) am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
} }
@ -691,6 +996,7 @@ type ClientAccount struct {
Credentials AccountCredentials Credentials AccountCredentials
Verified bool Verified bool
AdditionalNicks []string AdditionalNicks []string
VHost VHostInfo
} }
// convenience for passing around raw serialized account data // convenience for passing around raw serialized account data
@ -701,14 +1007,7 @@ type rawClientAccount struct {
Callback string Callback string
Verified bool Verified bool
AdditionalNicks string AdditionalNicks string
} VHost 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()
}
} }
// logoutOfAccount logs the client out of their current account. // logoutOfAccount logs the client out of their current account.

View File

@ -5,7 +5,6 @@ package irc
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
"github.com/goshuirc/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircfmt"
@ -22,22 +21,8 @@ To see in-depth help for a specific ChanServ command, try:
Here are the commands you can use: Here are the commands you can use:
%s` %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 ( var (
chanservCommands = map[string]*csCommand{ chanservCommands = map[string]*serviceCommand{
"help": {
help: `Syntax: $bHELP [command]$b
HELP returns information on the given command.`,
helpShort: `$bHELP$b shows in-depth information about commands.`,
},
"op": { "op": {
handler: csOpHandler, handler: csOpHandler,
help: `Syntax: $bOP #channel [nickname]$b help: `Syntax: $bOP #channel [nickname]$b
@ -45,6 +30,7 @@ HELP returns information on the given command.`,
OP makes the given nickname, or yourself, a channel admin. You can only use OP makes the given nickname, or yourself, a channel admin. You can only use
this command if you're the founder of the channel.`, 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": { "register": {
handler: csRegisterHandler, handler: csRegisterHandler,
@ -54,6 +40,7 @@ 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 given admin privs on it. Modes set on the channel and the topic will also be
remembered.`, 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) 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) { func csOpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
channelName, clientToOp := utils.ExtractParam(params) channelName, clientToOp := utils.ExtractParam(params)
@ -171,13 +73,7 @@ func csOpHandler(server *Server, client *Client, command, params string, rb *Res
} }
clientAccount := client.Account() clientAccount := client.Account()
if clientAccount == "" || clientAccount != channelInfo.Founder() {
if clientAccount == "" {
csNotice(rb, client.t("You must be logged in to op on a channel"))
return
}
if clientAccount != channelInfo.Founder() {
csNotice(rb, client.t("You must be the channel founder to op")) csNotice(rb, client.t("You must be the channel founder to op"))
return return
} }
@ -239,11 +135,6 @@ func csRegisterHandler(server *Server, client *Client, command, params string, r
return 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: // this provides the synchronization that allows exactly one registration of the channel:
err = channelInfo.SetRegistered(client.Account()) err = channelInfo.SetRegistered(client.Account())
if err != nil { if err != nil {

View File

@ -46,7 +46,6 @@ type Client struct {
capVersion caps.Version capVersion caps.Version
certfp string certfp string
channels ChannelSet channels ChannelSet
class *OperClass
ctime time.Time ctime time.Time
exitedSnomaskSent bool exitedSnomaskSent bool
fakelag *Fakelag fakelag *Fakelag
@ -65,7 +64,7 @@ type Client struct {
nickMaskCasefolded string nickMaskCasefolded string
nickMaskString string // cache for nickmask string since it's used with lots of replies nickMaskString string // cache for nickmask string since it's used with lots of replies
nickTimer *NickTimer nickTimer *NickTimer
operName string oper *Oper
preregNick string preregNick string
proxiedIP net.IP // actual remote IP if using the PROXY protocol proxiedIP net.IP // actual remote IP if using the PROXY protocol
quitMessage string quitMessage string
@ -81,7 +80,6 @@ type Client struct {
stateMutex sync.RWMutex // tier 1 stateMutex sync.RWMutex // tier 1
username string username string
vhost string vhost string
whoisLine string
} }
// NewClient returns a client with all the appropriate info setup. // 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. // Register sets the client details as appropriate when entering the network.
func (client *Client) Register() { func (client *Client) Register() {
client.stateMutex.Lock() client.stateMutex.Lock()
@ -495,12 +489,13 @@ func (client *Client) HasUsername() bool {
// HasRoleCapabs returns true if client has the given (role) capabilities. // HasRoleCapabs returns true if client has the given (role) capabilities.
func (client *Client) HasRoleCapabs(capabs ...string) bool { func (client *Client) HasRoleCapabs(capabs ...string) bool {
if client.class == nil { oper := client.Oper()
if oper == nil {
return false return false
} }
for _, capab := range capabs { for _, capab := range capabs {
if !client.class.Capabilities[capab] { if !oper.Class.Capabilities[capab] {
return false return false
} }
} }
@ -547,12 +542,45 @@ func (client *Client) Friends(capabs ...caps.Capability) ClientSet {
return friends 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`. // updateNick updates `nick` and `nickCasefolded`.
func (client *Client) updateNick(nick string) { func (client *Client) updateNick(nick string) {
casefoldedName, err := CasefoldName(nick) casefoldedName, err := CasefoldName(nick)
if err != nil { if err != nil {
log.Println(fmt.Sprintf("ERROR: Nick [%s] couldn't be casefolded... this should never happen. Printing stacktrace.", client.nick)) client.server.logger.Error("internal", "nick couldn't be casefolded", nick, err.Error())
debug.PrintStack() return
} }
client.stateMutex.Lock() client.stateMutex.Lock()
client.nick = nick client.nick = nick
@ -573,19 +601,18 @@ func (client *Client) updateNickMask(nick string) {
client.updateNickMaskNoMutex() 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() { func (client *Client) updateNickMaskNoMutex() {
if len(client.vhost) > 0 { client.hostname = client.getVHostNoMutex()
client.hostname = client.vhost if client.hostname == "" {
} else {
client.hostname = client.rawHostname client.hostname = client.rawHostname
} }
nickMaskString := fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.hostname) nickMaskString := fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.hostname)
nickMaskCasefolded, err := Casefold(nickMaskString) nickMaskCasefolded, err := Casefold(nickMaskString)
if err != nil { if err != nil {
log.Println(fmt.Sprintf("ERROR: Nickmask [%s] couldn't be casefolded... this should never happen. Printing stacktrace.", client.nickMaskString)) client.server.logger.Error("internal", "nickmask couldn't be casefolded", nickMaskString, err.Error())
debug.PrintStack() return
} }
client.nickMaskString = nickMaskString client.nickMaskString = nickMaskString
@ -598,19 +625,26 @@ func (client *Client) AllNickmasks() []string {
var mask string var mask string
var err error var err error
if len(client.vhost) > 0 { client.stateMutex.RLock()
mask, err = Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.vhost)) 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 { if err == nil {
masks = append(masks, mask) 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 { if err == nil {
masks = append(masks, mask) 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 { if err == nil && mask2 != mask {
masks = append(masks, mask2) 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. // SendFromClient sends an IRC line coming from a specific client.
// Adds account-tag to the line as well. // 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 { 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 // attach account-tag
if client.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() { if client.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() {
if tags == nil { 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 ( var (

View File

@ -92,14 +92,6 @@ func init() {
usablePreReg: true, usablePreReg: true,
minParams: 1, minParams: 1,
}, },
"CHANSERV": {
handler: csHandler,
minParams: 1,
},
"CS": {
handler: csHandler,
minParams: 1,
},
"DEBUG": { "DEBUG": {
handler: debugHandler, handler: debugHandler,
minParams: 1, minParams: 1,
@ -182,10 +174,6 @@ func init() {
usablePreReg: true, usablePreReg: true,
minParams: 1, minParams: 1,
}, },
"NICKSERV": {
handler: nsHandler,
minParams: 1,
},
"NOTICE": { "NOTICE": {
handler: noticeHandler, handler: noticeHandler,
minParams: 2, minParams: 2,
@ -198,10 +186,6 @@ func init() {
handler: npcaHandler, handler: npcaHandler,
minParams: 3, minParams: 3,
}, },
"NS": {
handler: nsHandler,
minParams: 1,
},
"OPER": { "OPER": {
handler: operHandler, handler: operHandler,
minParams: 2, minParams: 2,
@ -323,4 +307,6 @@ func init() {
minParams: 1, minParams: 1,
}, },
} }
initializeServices()
} }

View File

@ -13,6 +13,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
@ -64,6 +65,7 @@ type AccountConfig struct {
AuthenticationEnabled bool `yaml:"authentication-enabled"` AuthenticationEnabled bool `yaml:"authentication-enabled"`
SkipServerPassword bool `yaml:"skip-server-password"` SkipServerPassword bool `yaml:"skip-server-password"`
NickReservation NickReservationConfig `yaml:"nick-reservation"` NickReservation NickReservationConfig `yaml:"nick-reservation"`
VHosts VHostConfig
} }
// AccountRegistrationConfig controls account registration. // AccountRegistrationConfig controls account registration.
@ -91,6 +93,18 @@ type AccountRegistrationConfig struct {
AllowMultiplePerConnection bool `yaml:"allow-multiple-per-connection"` 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 type NickReservationMethod int
const ( const (
@ -278,8 +292,8 @@ type OperClass struct {
} }
// OperatorClasses returns a map of assembled operator classes from the given config. // OperatorClasses returns a map of assembled operator classes from the given config.
func (conf *Config) OperatorClasses() (*map[string]OperClass, error) { func (conf *Config) OperatorClasses() (map[string]*OperClass, error) {
ocs := make(map[string]OperClass) ocs := make(map[string]*OperClass)
// loop from no extends to most extended, breaking if we can't add any more // loop from no extends to most extended, breaking if we can't add any more
lenOfLastOcs := -1 lenOfLastOcs := -1
@ -335,7 +349,7 @@ func (conf *Config) OperatorClasses() (*map[string]OperClass, error) {
oc.WhoisLine += oc.Title oc.WhoisLine += oc.Title
} }
ocs[name] = oc ocs[name] = &oc
} }
if !anyMissing { 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. // Oper represents a single assembled operator's config.
type Oper struct { type Oper struct {
Name string
Class *OperClass Class *OperClass
WhoisLine string WhoisLine string
Vhost string Vhost string
@ -357,8 +372,8 @@ type Oper struct {
} }
// Operators returns a map of operator configs from the given OperClass and config. // 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) { func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error) {
operators := make(map[string]Oper) operators := make(map[string]*Oper)
for name, opConf := range conf.Opers { for name, opConf := range conf.Opers {
var oper Oper var oper Oper
@ -367,14 +382,15 @@ func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error)
if err != nil { if err != nil {
return nil, fmt.Errorf("Could not casefold oper name: %s", err.Error()) return nil, fmt.Errorf("Could not casefold oper name: %s", err.Error())
} }
oper.Name = name
oper.Pass = opConf.PasswordBytes() oper.Pass = opConf.PasswordBytes()
oper.Vhost = opConf.Vhost oper.Vhost = opConf.Vhost
class, exists := (*oc)[opConf.Class] class, exists := oc[opConf.Class]
if !exists { if !exists {
return nil, fmt.Errorf("Could not load operator [%s] - they use operclass [%s] which does not exist", name, opConf.Class) 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 { if len(opConf.WhoisLine) > 0 {
oper.WhoisLine = opConf.WhoisLine oper.WhoisLine = opConf.WhoisLine
} else { } else {
@ -388,7 +404,7 @@ func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error)
oper.Modes = modeChanges oper.Modes = modeChanges
// successful, attach to list of opers // successful, attach to list of opers
operators[name] = oper operators[name] = &oper
} }
return operators, nil 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) maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
if err != nil { if err != nil {
return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error()) return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())

View File

@ -21,6 +21,7 @@ var (
errAccountTooManyNicks = errors.New("Account has too many reserved nicks") errAccountTooManyNicks = errors.New("Account has too many reserved nicks")
errAccountNickReservationFailed = errors.New("Could not (un)reserve nick") errAccountNickReservationFailed = errors.New("Could not (un)reserve nick")
errAccountCantDropPrimaryNick = errors.New("Can't unreserve primary nickname") 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") errCallbackFailed = errors.New("Account verification could not be sent")
errCertfpAlreadyExists = errors.New("An account already exists with your certificate") errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
errChannelAlreadyRegistered = errors.New("Channel is already registered") errChannelAlreadyRegistered = errors.New("Channel is already registered")

View File

@ -75,9 +75,12 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (exiting bool)
} }
// given IP is sane! override the client's current IP // given IP is sane! override the client's current IP
rawHostname := utils.LookupHostname(proxiedIP)
client.stateMutex.Lock()
client.proxiedIP = parsedProxiedIP client.proxiedIP = parsedProxiedIP
client.rawHostname = utils.LookupHostname(proxiedIP) client.rawHostname = rawHostname
client.hostname = client.rawHostname client.stateMutex.Unlock()
// nickmask will be updated when the client completes registration
// set tls info // set tls info
client.certfp = "" client.certfp = ""

View File

@ -83,6 +83,16 @@ func (server *Server) FakelagConfig() *FakelagConfig {
return &server.config.Fakelag 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 { func (client *Client) Nick() string {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
@ -119,6 +129,18 @@ func (client *Client) Realname() string {
return client.realname 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 { func (client *Client) Registered() bool {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()

View File

@ -31,6 +31,7 @@ import (
"github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/sno"
"github.com/oragono/oragono/irc/utils" "github.com/oragono/oragono/irc/utils"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
"golang.org/x/crypto/bcrypt"
) )
// ACC [REGISTER|VERIFY] ... // ACC [REGISTER|VERIFY] ...
@ -494,12 +495,6 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
return false 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> // DEBUG <subcmd>
func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
param := strings.ToUpper(msg.Params[0]) param := strings.ToUpper(msg.Params[0])
@ -562,7 +557,8 @@ func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
// DLINE LIST // DLINE LIST
func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
// check oper permissions // 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")) rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
return false 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 == "" { if operName == "" {
operName = server.name operName = server.name
} }
@ -977,7 +973,8 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
// KLINE LIST // KLINE LIST
func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
// check oper permissions // 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")) rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
return false return false
} }
@ -1052,7 +1049,7 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
} }
// get oper name // get oper name
operName := client.operName operName := oper.Name
if operName == "" { if operName == "" {
operName = server.name operName = server.name
} }
@ -1648,11 +1645,9 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
if err != nil { if err != nil {
continue continue
} }
if target == "chanserv" {
server.chanservNoticeHandler(client, message, rb) // NOTICEs sent to services are ignored
continue if _, isService := OragonoServices[target]; isService {
} else if target == "nickserv" {
server.nickservNoticeHandler(client, message, rb)
continue continue
} }
@ -1715,46 +1710,29 @@ func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
return false 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> // OPER <name> <password>
func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { 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 { if client.HasMode(modes.Operator) == true {
rb.Add(nil, server.name, ERR_UNKNOWNERROR, "OPER", client.t("You're already opered-up!")) rb.Add(nil, server.name, ERR_UNKNOWNERROR, "OPER", client.t("You're already opered-up!"))
return false return false
} }
server.configurableStateMutex.RLock()
oper := server.operators[name]
server.configurableStateMutex.RUnlock()
authorized := false
oper := server.GetOperator(msg.Params[0])
if oper != nil {
password := []byte(msg.Params[1]) password := []byte(msg.Params[1])
err = passwd.ComparePassword(oper.Pass, password) authorized = (bcrypt.CompareHashAndPassword(oper.Pass, password) == nil)
if (oper.Pass == nil) || (err != nil) { }
if !authorized {
rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect")) rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect"))
return true return true
} }
client.operName = name oldNickmask := client.NickMaskString()
client.class = oper.Class client.SetOper(oper)
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("") client.updateNickMask("")
if client.NickMaskString() != oldNickmask {
client.sendChghost(oldNickmask, oper.Vhost)
} }
// set new modes: modes.Operator, plus anything specified in the config // 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, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator"))
rb.Add(nil, server.name, "MODE", client.nick, applied.String()) 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 may now be unthrottled by the fakelag system
client.resetFakelag() 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) channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg, rb)
} else { } else {
target, err = CasefoldName(targetString) target, err = CasefoldName(targetString)
if target == "chanserv" { if service, isService := OragonoServices[target]; isService {
server.chanservPrivmsgHandler(client, message, rb) servicePrivmsgHandler(service, server, client, message, rb)
continue
} else if target == "nickserv" {
server.nickservPrivmsgHandler(client, message, rb)
continue continue
} }
user := server.clients.Get(target) user := server.clients.Get(target)
@ -2179,7 +2154,8 @@ func topicHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
// UNDLINE <ip>|<net> // UNDLINE <ip>|<net>
func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
// check oper permissions // 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")) rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
return false return false
} }
@ -2242,7 +2218,8 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
// UNKLINE <mask> // UNKLINE <mask>
func unKLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func unKLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
// check oper permissions // 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")) rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
return false return false
} }

View File

@ -187,6 +187,18 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
text: `HELPOP <argument> text: `HELPOP <argument>
Get an explanation of <argument>, or "index" for a list of help topics.`, 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": { "info": {
text: `INFO text: `INFO

314
irc/hostserv.go Normal file
View 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))
}
}
}
}

View File

@ -17,9 +17,6 @@ import (
var ( var (
restrictedNicknames = map[string]bool{ restrictedNicknames = map[string]bool{
"=scene=": true, // used for rp commands "=scene=": true, // used for rp commands
"chanserv": true,
"nickserv": true,
"hostserv": true,
} }
) )

View File

@ -5,15 +5,20 @@ package irc
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
"github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/modes"
"github.com/oragono/oragono/irc/utils" "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. const nickservHelp = `NickServ lets you register and login to an account.
To see in-depth help for a specific NickServ command, try: 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: Here are the commands you can use:
%s` %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 ( var (
nickservCommands = map[string]*nsCommand{ nickservCommands = map[string]*serviceCommand{
"drop": { "drop": {
handler: nsDropHandler, handler: nsDropHandler,
help: `Syntax: $bDROP [nickname]$b help: `Syntax: $bDROP [nickname]$b
DROP de-links the given (or your current) nickname from your user account.`, 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.`, helpShort: `$bDROP$b de-links your current (or the given) nickname from your user account.`,
nickReservation: true, enabled: servCmdRequiresAccreg,
authRequired: true,
}, },
"ghost": { "ghost": {
handler: nsGhostHandler, handler: nsGhostHandler,
@ -48,6 +45,7 @@ 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 GHOST disconnects the given user from the network if they're logged in with the
same user account, letting you reclaim your nickname.`, same user account, letting you reclaim your nickname.`,
helpShort: `$bGHOST$b reclaims your nickname.`, helpShort: `$bGHOST$b reclaims your nickname.`,
authRequired: true,
}, },
"group": { "group": {
handler: nsGroupHandler, handler: nsGroupHandler,
@ -56,14 +54,10 @@ same user account, letting you reclaim your nickname.`,
GROUP links your current nickname with your logged-in account, preventing other GROUP links your current nickname with your logged-in account, preventing other
users from changing to it (or forcing them to rename).`, users from changing to it (or forcing them to rename).`,
helpShort: `$bGROUP$b links your current nickname to your user account.`, helpShort: `$bGROUP$b links your current nickname to your user account.`,
nickReservation: true, 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": { "identify": {
handler: nsIdentifyHandler, handler: nsIdentifyHandler,
help: `Syntax: $bIDENTIFY <username> [password]$b 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 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).`, certificate (and you will need to use that certificate to login in future).`,
helpShort: `$bREGISTER$b lets you register a user account.`, helpShort: `$bREGISTER$b lets you register a user account.`,
enabled: servCmdRequiresAccreg,
}, },
"sadrop": { "sadrop": {
handler: nsDropHandler, handler: nsDropHandler,
help: `Syntax: $bSADROP <nickname>$b help: `Syntax: $bSADROP <nickname>$b
SADROP foribly de-links the given nickname from the attached user account.`, 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.`, helpShort: `$bSADROP$b forcibly de-links the given nickname from its user account.`,
nickReservation: true,
capabs: []string{"unregister"}, capabs: []string{"unregister"},
enabled: servCmdRequiresAccreg,
}, },
"unregister": { "unregister": {
handler: nsUnregisterHandler, handler: nsUnregisterHandler,
@ -116,6 +111,7 @@ IRC operator with the correct permissions).`,
VERIFY lets you complete an account registration, if the server requires email VERIFY lets you complete an account registration, if the server requires email
or other verification.`, or other verification.`,
helpShort: `$bVERIFY$b lets you complete account registration.`, 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) 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) { func nsDropHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
sadrop := command == "sadrop" sadrop := command == "sadrop"
nick, _ := utils.ExtractParam(params) 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) { 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() nick := client.NickCasefolded()
err := server.accounts.SetNickReserved(client, nick, false, true) err := server.accounts.SetNickReserved(client, nick, false, true)
if err == nil { 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) { 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 loginSuccessful := false
username, passphrase := utils.ExtractParam(params) username, passphrase := utils.ExtractParam(params)

View File

@ -115,8 +115,8 @@ type Server struct {
name string name string
nameCasefolded string nameCasefolded string
networkName string networkName string
operators map[string]Oper operators map[string]*Oper
operclasses map[string]OperClass operclasses map[string]*OperClass
password []byte password []byte
passwords *passwd.SaltedManager passwords *passwd.SaltedManager
recoverFromErrors bool recoverFromErrors bool
@ -659,8 +659,9 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
if whoischannels != nil { if whoischannels != nil {
rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " ")) rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " "))
} }
if target.class != nil { tOper := target.Oper()
rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine) if tOper != nil {
rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, tOper.WhoisLine)
} }
if client.HasMode(modes.Operator) || client == target { 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")) 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() server.accounts.buildNickToAccountIndex()
} }
hsPreviouslyDisabled := oldAccountConfig != nil && !oldAccountConfig.VHosts.Enabled
hsNowEnabled := config.Accounts.VHosts.Enabled
if hsPreviouslyDisabled && hsNowEnabled {
server.accounts.initVHostRequestQueue()
}
// STS // STS
stsValue := config.Server.STS.Value() stsValue := config.Server.STS.Value()
var stsDisabled bool var stsDisabled bool
@ -944,7 +951,7 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
ChanListModes: int(config.Limits.ChanListModes), ChanListModes: int(config.Limits.ChanListModes),
LineLen: lineLenConfig, LineLen: lineLenConfig,
} }
server.operclasses = *operclasses server.operclasses = operclasses
server.operators = opers server.operators = opers
server.checkIdent = config.Server.CheckIdent server.checkIdent = config.Server.CheckIdent

194
irc/services.go Normal file
View 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
}
}
}

View File

@ -198,6 +198,36 @@ accounts:
# rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31) # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
rename-prefix: Guest- 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 # channel options
channels: channels:
# modes that are set when new channels are created # modes that are set when new channels are created
@ -252,6 +282,7 @@ oper-classes:
- "oper:die" - "oper:die"
- "unregister" - "unregister"
- "samode" - "samode"
- "vhosts"
# ircd operators # ircd operators
opers: opers: