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:
commit
de7b679fc5
331
irc/accounts.go
331
irc/accounts.go
@ -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.
|
||||||
|
117
irc/chanserv.go
117
irc/chanserv.go
@ -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 {
|
||||||
|
@ -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 (
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
@ -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")
|
||||||
|
@ -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 = ""
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
12
irc/help.go
12
irc/help.go
@ -187,6 +187,18 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
|
|||||||
text: `HELPOP <argument>
|
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
314
irc/hostserv.go
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oragono/oragono/irc/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const hostservHelp = `HostServ lets you manage your vhost (i.e., the string displayed
|
||||||
|
in place of your client's hostname/IP).
|
||||||
|
|
||||||
|
To see in-depth help for a specific HostServ command, try:
|
||||||
|
$b/HS HELP <command>$b
|
||||||
|
|
||||||
|
Here are the commands you can use:
|
||||||
|
%s`
|
||||||
|
|
||||||
|
var (
|
||||||
|
errVHostBadCharacters = errors.New("Vhost contains prohibited characters")
|
||||||
|
errVHostTooLong = errors.New("Vhost is too long")
|
||||||
|
// ascii only for now
|
||||||
|
defaultValidVhostRegex = regexp.MustCompile(`^[0-9A-Za-z.\-_/]+$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func hostservEnabled(server *Server) bool {
|
||||||
|
return server.AccountConfig().VHosts.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostservRequestsEnabled(server *Server) bool {
|
||||||
|
ac := server.AccountConfig()
|
||||||
|
return ac.VHosts.Enabled && ac.VHosts.UserRequests.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hostservCommands = map[string]*serviceCommand{
|
||||||
|
"on": {
|
||||||
|
handler: hsOnOffHandler,
|
||||||
|
help: `Syntax: $bON$b
|
||||||
|
|
||||||
|
ON enables your vhost, if you have one approved.`,
|
||||||
|
helpShort: `$bON$b enables your vhost, if you have one approved.`,
|
||||||
|
authRequired: true,
|
||||||
|
enabled: hostservEnabled,
|
||||||
|
},
|
||||||
|
"off": {
|
||||||
|
handler: hsOnOffHandler,
|
||||||
|
help: `Syntax: $bOFF$b
|
||||||
|
|
||||||
|
OFF disables your vhost, if you have one approved.`,
|
||||||
|
helpShort: `$bOFF$b disables your vhost, if you have one approved.`,
|
||||||
|
authRequired: true,
|
||||||
|
enabled: hostservEnabled,
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
handler: hsRequestHandler,
|
||||||
|
help: `Syntax: $bREQUEST <vhost>$b
|
||||||
|
|
||||||
|
REQUEST requests that a new vhost by assigned to your account. The request must
|
||||||
|
then be approved by a server operator.`,
|
||||||
|
helpShort: `$bREQUEST$b requests a new vhost, pending operator approval.`,
|
||||||
|
authRequired: true,
|
||||||
|
enabled: hostservRequestsEnabled,
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
handler: hsStatusHandler,
|
||||||
|
help: `Syntax: $bSTATUS$b
|
||||||
|
|
||||||
|
STATUS displays your current vhost, if any, and the status of your most recent
|
||||||
|
request for a new one.`,
|
||||||
|
helpShort: `$bSTATUS$b shows your vhost and request status.`,
|
||||||
|
authRequired: true,
|
||||||
|
enabled: hostservEnabled,
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
handler: hsSetHandler,
|
||||||
|
help: `Syntax: $bSET <user> <vhost>$b
|
||||||
|
|
||||||
|
SET sets a user's vhost, bypassing the request system.`,
|
||||||
|
helpShort: `$bSET$b sets a user's vhost.`,
|
||||||
|
capabs: []string{"vhosts"},
|
||||||
|
enabled: hostservEnabled,
|
||||||
|
},
|
||||||
|
"del": {
|
||||||
|
handler: hsSetHandler,
|
||||||
|
help: `Syntax: $bDEL <user>$b
|
||||||
|
|
||||||
|
DEL sets a user's vhost, bypassing the request system.`,
|
||||||
|
helpShort: `$bDEL$b deletes a user's vhost.`,
|
||||||
|
capabs: []string{"vhosts"},
|
||||||
|
enabled: hostservEnabled,
|
||||||
|
},
|
||||||
|
"waiting": {
|
||||||
|
handler: hsWaitingHandler,
|
||||||
|
help: `Syntax: $bWAITING$b
|
||||||
|
|
||||||
|
WAITING shows a list of pending vhost requests, which can then be approved
|
||||||
|
or rejected.`,
|
||||||
|
helpShort: `$bWAITING$b shows a list of pending vhost requests.`,
|
||||||
|
capabs: []string{"vhosts"},
|
||||||
|
enabled: hostservEnabled,
|
||||||
|
},
|
||||||
|
"approve": {
|
||||||
|
handler: hsApproveHandler,
|
||||||
|
help: `Syntax: $bAPPROVE <user>$b
|
||||||
|
|
||||||
|
APPROVE approves a user's vhost request.`,
|
||||||
|
helpShort: `$bAPPROVE$b approves a user's vhost request.`,
|
||||||
|
capabs: []string{"vhosts"},
|
||||||
|
enabled: hostservEnabled,
|
||||||
|
},
|
||||||
|
"reject": {
|
||||||
|
handler: hsRejectHandler,
|
||||||
|
help: `Syntax: $bREJECT <user> [<reason>]$b
|
||||||
|
|
||||||
|
REJECT rejects a user's vhost request, optionally giving them a reason
|
||||||
|
for the rejection.`,
|
||||||
|
helpShort: `$bREJECT$b rejects a user's vhost request.`,
|
||||||
|
capabs: []string{"vhosts"},
|
||||||
|
enabled: hostservEnabled,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// hsNotice sends the client a notice from HostServ
|
||||||
|
func hsNotice(rb *ResponseBuffer, text string) {
|
||||||
|
rb.Add(nil, "HostServ", "NOTICE", rb.target.Nick(), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hsNotifyChannel notifies the designated channel of new vhost activity
|
||||||
|
func hsNotifyChannel(server *Server, message string) {
|
||||||
|
chname := server.AccountConfig().VHosts.UserRequests.Channel
|
||||||
|
channel := server.channels.Get(chname)
|
||||||
|
if channel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chname = channel.Name()
|
||||||
|
for _, client := range channel.Members() {
|
||||||
|
client.Send(nil, "HostServ", "PRIVMSG", chname, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hsOnOffHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||||
|
enable := false
|
||||||
|
if command == "on" {
|
||||||
|
enable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := server.accounts.VHostSetEnabled(client, enable)
|
||||||
|
if err != nil {
|
||||||
|
hsNotice(rb, client.t("An error occurred"))
|
||||||
|
} else if enable {
|
||||||
|
hsNotice(rb, client.t("Successfully enabled your vhost"))
|
||||||
|
} else {
|
||||||
|
hsNotice(rb, client.t("Successfully disabled your vhost"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hsRequestHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||||
|
vhost, _ := utils.ExtractParam(params)
|
||||||
|
if validateVhost(server, vhost, false) != nil {
|
||||||
|
hsNotice(rb, client.t("Invalid vhost"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accountName := client.Account()
|
||||||
|
account, err := server.accounts.LoadAccount(client.Account())
|
||||||
|
if err != nil {
|
||||||
|
hsNotice(rb, client.t("An error occurred"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elapsed := time.Now().Sub(account.VHost.LastRequestTime)
|
||||||
|
remainingTime := server.AccountConfig().VHosts.UserRequests.Cooldown - elapsed
|
||||||
|
// you can update your existing request, but if you were rejected,
|
||||||
|
// you can't spam a replacement request
|
||||||
|
if account.VHost.RequestedVHost == "" && remainingTime > 0 {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), remainingTime))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = server.accounts.VHostRequest(accountName, vhost)
|
||||||
|
if err != nil {
|
||||||
|
hsNotice(rb, client.t("An error occurred"))
|
||||||
|
} else {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("Your vhost request will be reviewed by an administrator")))
|
||||||
|
chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost)
|
||||||
|
hsNotifyChannel(server, chanMsg)
|
||||||
|
// TODO send admins a snomask of some kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hsStatusHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||||
|
accountName := client.Account()
|
||||||
|
account, err := server.accounts.LoadAccount(accountName)
|
||||||
|
if err != nil {
|
||||||
|
server.logger.Warning("internal", "error loading account info", accountName, err.Error())
|
||||||
|
hsNotice(rb, client.t("An error occurred"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.VHost.ApprovedVHost != "" {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("Account %s has vhost: %s"), accountName, account.VHost.ApprovedVHost))
|
||||||
|
if !account.VHost.Enabled {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("This vhost is currently disabled, but can be enabled with /HS ON")))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName))
|
||||||
|
}
|
||||||
|
if account.VHost.RequestedVHost != "" {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("A request is pending for vhost: %s"), account.VHost.RequestedVHost))
|
||||||
|
}
|
||||||
|
if account.VHost.RejectedVHost != "" {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("A request was previously made for vhost: %s"), account.VHost.RejectedVHost))
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("It was rejected for reason: %s"), account.VHost.RejectionReason))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateVhost(server *Server, vhost string, oper bool) error {
|
||||||
|
ac := server.AccountConfig()
|
||||||
|
if len(vhost) > ac.VHosts.MaxLength {
|
||||||
|
return errVHostTooLong
|
||||||
|
}
|
||||||
|
if !ac.VHosts.ValidRegexp.MatchString(vhost) {
|
||||||
|
return errVHostBadCharacters
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hsSetHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||||
|
var user, vhost string
|
||||||
|
user, params = utils.ExtractParam(params)
|
||||||
|
if user == "" {
|
||||||
|
hsNotice(rb, client.t("A user is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if command == "set" {
|
||||||
|
vhost, _ = utils.ExtractParam(params)
|
||||||
|
if validateVhost(server, vhost, true) != nil {
|
||||||
|
hsNotice(rb, client.t("Invalid vhost"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if command != "del" {
|
||||||
|
server.logger.Warning("internal", "invalid hostserv set command", command)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := server.accounts.VHostSet(user, vhost)
|
||||||
|
if err != nil {
|
||||||
|
hsNotice(rb, client.t("An error occurred"))
|
||||||
|
} else if vhost != "" {
|
||||||
|
hsNotice(rb, client.t("Successfully set vhost"))
|
||||||
|
} else {
|
||||||
|
hsNotice(rb, client.t("Successfully cleared vhost"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hsWaitingHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||||
|
requests, total := server.accounts.VHostListRequests(10)
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("There are %d pending requests for vhosts (%d displayed)"), total, len(requests)))
|
||||||
|
for i, request := range requests {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("%d. User %s requests vhost: %s"), i+1, request.Account, request.RequestedVHost))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hsApproveHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||||
|
user, _ := utils.ExtractParam(params)
|
||||||
|
if user == "" {
|
||||||
|
hsNotice(rb, client.t("A user is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vhostInfo, err := server.accounts.VHostApprove(user)
|
||||||
|
if err != nil {
|
||||||
|
hsNotice(rb, client.t("An error occurred"))
|
||||||
|
} else {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("Successfully approved vhost request for %s"), user))
|
||||||
|
chanMsg := fmt.Sprintf("Oper %s approved vhost %s for account %s", client.Nick(), vhostInfo.ApprovedVHost, user)
|
||||||
|
hsNotifyChannel(server, chanMsg)
|
||||||
|
for _, client := range server.accounts.AccountToClients(user) {
|
||||||
|
client.Notice(client.t("Your vhost request was approved by an administrator"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hsRejectHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
|
||||||
|
user, params := utils.ExtractParam(params)
|
||||||
|
if user == "" {
|
||||||
|
hsNotice(rb, client.t("A user is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reason := strings.TrimSpace(params)
|
||||||
|
|
||||||
|
vhostInfo, err := server.accounts.VHostReject(user, reason)
|
||||||
|
if err != nil {
|
||||||
|
hsNotice(rb, client.t("An error occurred"))
|
||||||
|
} else {
|
||||||
|
hsNotice(rb, fmt.Sprintf(client.t("Successfully rejected vhost request for %s"), user))
|
||||||
|
chanMsg := fmt.Sprintf("Oper %s rejected vhost %s for account %s, with the reason: %v", client.Nick(), vhostInfo.RejectedVHost, user, reason)
|
||||||
|
hsNotifyChannel(server, chanMsg)
|
||||||
|
for _, client := range server.accounts.AccountToClients(user) {
|
||||||
|
if reason == "" {
|
||||||
|
client.Notice("Your vhost request was rejected by an administrator")
|
||||||
|
} else {
|
||||||
|
client.Notice(fmt.Sprintf(client.t("Your vhost request was rejected by an administrator. The reason given was: %s"), reason))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
147
irc/nickserv.go
147
irc/nickserv.go
@ -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)
|
||||||
|
@ -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
194
irc/services.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goshuirc/irc-go/ircfmt"
|
||||||
|
"github.com/goshuirc/irc-go/ircmsg"
|
||||||
|
|
||||||
|
"github.com/oragono/oragono/irc/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defines an IRC service, e.g., NICKSERV
|
||||||
|
type ircService struct {
|
||||||
|
Name string
|
||||||
|
ShortName string
|
||||||
|
CommandAliases []string
|
||||||
|
Commands map[string]*serviceCommand
|
||||||
|
HelpBanner string
|
||||||
|
}
|
||||||
|
|
||||||
|
// defines a command associated with a service, e.g., NICKSERV IDENTIFY
|
||||||
|
type serviceCommand struct {
|
||||||
|
capabs []string // oper capabs the given user has to have to access this command
|
||||||
|
handler func(server *Server, client *Client, command, params string, rb *ResponseBuffer)
|
||||||
|
help string
|
||||||
|
helpShort string
|
||||||
|
authRequired bool
|
||||||
|
enabled func(*Server) bool // is this command enabled in the server config?
|
||||||
|
}
|
||||||
|
|
||||||
|
// all services, by lowercase name
|
||||||
|
var OragonoServices = map[string]*ircService{
|
||||||
|
"nickserv": {
|
||||||
|
Name: "NickServ",
|
||||||
|
ShortName: "NS",
|
||||||
|
CommandAliases: []string{"NICKSERV", "NS"},
|
||||||
|
Commands: nickservCommands,
|
||||||
|
HelpBanner: nickservHelp,
|
||||||
|
},
|
||||||
|
"chanserv": {
|
||||||
|
Name: "ChanServ",
|
||||||
|
ShortName: "CS",
|
||||||
|
CommandAliases: []string{"CHANSERV", "CS"},
|
||||||
|
Commands: chanservCommands,
|
||||||
|
HelpBanner: chanservHelp,
|
||||||
|
},
|
||||||
|
"hostserv": {
|
||||||
|
Name: "HostServ",
|
||||||
|
ShortName: "HS",
|
||||||
|
CommandAliases: []string{"HOSTSERV", "HS"},
|
||||||
|
Commands: hostservCommands,
|
||||||
|
HelpBanner: hostservHelp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// all service commands at the protocol level, by uppercase command name
|
||||||
|
// e.g., NICKSERV, NS
|
||||||
|
var oragonoServicesByCommandAlias map[string]*ircService
|
||||||
|
|
||||||
|
// special-cased command shared by all services
|
||||||
|
var servHelpCmd serviceCommand = serviceCommand{
|
||||||
|
help: `Syntax: $bHELP [command]$b
|
||||||
|
|
||||||
|
HELP returns information on the given command.`,
|
||||||
|
helpShort: `$bHELP$b shows in-depth information about commands.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// this handles IRC commands like `/NICKSERV INFO`, translating into `/MSG NICKSERV INFO`
|
||||||
|
func serviceCmdHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
|
service, ok := oragonoServicesByCommandAlias[msg.Command]
|
||||||
|
if !ok {
|
||||||
|
server.logger.Warning("internal", "can't handle unrecognized service", msg.Command)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fakePrivmsg := strings.Join(msg.Params, " ")
|
||||||
|
servicePrivmsgHandler(service, server, client, fakePrivmsg, rb)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// generic handler for service PRIVMSG
|
||||||
|
func servicePrivmsgHandler(service *ircService, server *Server, client *Client, message string, rb *ResponseBuffer) {
|
||||||
|
commandName, params := utils.ExtractParam(message)
|
||||||
|
commandName = strings.ToLower(commandName)
|
||||||
|
|
||||||
|
nick := rb.target.Nick()
|
||||||
|
sendNotice := func(notice string) {
|
||||||
|
rb.Add(nil, service.Name, "NOTICE", nick, notice)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := service.Commands[commandName]
|
||||||
|
if cmd == nil {
|
||||||
|
sendNotice(fmt.Sprintf("%s /%s HELP", client.t("Unknown command. To see available commands, run"), service.ShortName))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.enabled != nil && !cmd.enabled(server) {
|
||||||
|
sendNotice(client.t("This command has been disabled by the server administrators"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if 0 < len(cmd.capabs) && !client.HasRoleCapabs(cmd.capabs...) {
|
||||||
|
sendNotice(client.t("Command restricted"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.authRequired && client.Account() == "" {
|
||||||
|
sendNotice(client.t("You're not logged into an account"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server.logger.Debug("services", fmt.Sprintf("Client %s ran %s command %s", client.Nick(), service.Name, commandName))
|
||||||
|
if commandName == "help" {
|
||||||
|
serviceHelpHandler(service, server, client, params, rb)
|
||||||
|
} else {
|
||||||
|
cmd.handler(server, client, commandName, params, rb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generic handler that displays help for service commands
|
||||||
|
func serviceHelpHandler(service *ircService, server *Server, client *Client, params string, rb *ResponseBuffer) {
|
||||||
|
nick := rb.target.Nick()
|
||||||
|
sendNotice := func(notice string) {
|
||||||
|
rb.Add(nil, service.Name, "NOTICE", nick, notice)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
|
||||||
|
|
||||||
|
if params == "" {
|
||||||
|
// show general help
|
||||||
|
var shownHelpLines sort.StringSlice
|
||||||
|
for _, commandInfo := range service.Commands {
|
||||||
|
// skip commands user can't access
|
||||||
|
if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if commandInfo.enabled != nil && !commandInfo.enabled(server) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
shownHelpLines = append(shownHelpLines, " "+client.t(commandInfo.helpShort))
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort help lines
|
||||||
|
sort.Sort(shownHelpLines)
|
||||||
|
|
||||||
|
// assemble help text
|
||||||
|
assembledHelpLines := strings.Join(shownHelpLines, "\n")
|
||||||
|
fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(service.HelpBanner), assembledHelpLines))
|
||||||
|
|
||||||
|
// push out help text
|
||||||
|
for _, line := range strings.Split(fullHelp, "\n") {
|
||||||
|
sendNotice(line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commandInfo := service.Commands[strings.ToLower(strings.TrimSpace(params))]
|
||||||
|
if commandInfo == nil {
|
||||||
|
sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName)))
|
||||||
|
} else {
|
||||||
|
for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
|
||||||
|
sendNotice(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNotice(ircfmt.Unescape(fmt.Sprintf(client.t("*** $bEnd of %s HELP$b ***"), service.Name)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeServices() {
|
||||||
|
// this modifies the global Commands map,
|
||||||
|
// so it must be called from irc/commands.go's init()
|
||||||
|
oragonoServicesByCommandAlias = make(map[string]*ircService)
|
||||||
|
|
||||||
|
for serviceName, service := range OragonoServices {
|
||||||
|
// make `/MSG ServiceName HELP` work correctly
|
||||||
|
service.Commands["help"] = &servHelpCmd
|
||||||
|
|
||||||
|
// reserve the nickname
|
||||||
|
restrictedNicknames[serviceName] = true
|
||||||
|
|
||||||
|
// register the protocol-level commands (NICKSERV, NS) that talk to the service
|
||||||
|
var ircCmdDef Command
|
||||||
|
ircCmdDef.handler = serviceCmdHandler
|
||||||
|
for _, ircCmd := range service.CommandAliases {
|
||||||
|
Commands[ircCmd] = ircCmdDef
|
||||||
|
oragonoServicesByCommandAlias[ircCmd] = service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
oragono.yaml
31
oragono.yaml
@ -198,6 +198,36 @@ accounts:
|
|||||||
# rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
|
# rename-prefix - 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:
|
||||||
|
Loading…
Reference in New Issue
Block a user