3
0
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:
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"
"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.

View File

@ -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 {

View File

@ -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 (

View File

@ -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()
}

View File

@ -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())

View File

@ -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")

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
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 = ""

View File

@ -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()

View File

@ -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
}

View File

@ -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
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

@ -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
}
)

View File

@ -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)

View File

@ -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
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: 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: