mirror of
https://github.com/jeremyd/ergo.git
synced 2026-07-12 11:18:09 -07:00
Merge remote-tracking branch 'origin/master' into master+relaymsg
This commit is contained in:
+133
-88
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
"github.com/oragono/oragono/irc/connection_limits"
|
||||
"github.com/oragono/oragono/irc/email"
|
||||
"github.com/oragono/oragono/irc/ldap"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
@@ -40,7 +39,8 @@ const (
|
||||
keyAccountChannels = "account.channels %s" // channels registered to the account
|
||||
keyAccountJoinedChannels = "account.joinedto %s" // channels a persistent client has joined
|
||||
keyAccountLastSeen = "account.lastseen %s"
|
||||
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
||||
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
||||
keyAccountRealname = "account.realname %s" // client realname stored as string
|
||||
|
||||
keyVHostQueueAcctToId = "vhostQueue %s"
|
||||
vhostRequestIdx = "vhostQueue"
|
||||
@@ -128,7 +128,13 @@ func (am *AccountManager) createAlwaysOnClients(config *Config) {
|
||||
account, err := am.LoadAccount(accountName)
|
||||
if err == nil && account.Verified &&
|
||||
persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, account.Settings.AlwaysOn) {
|
||||
am.server.AddAlwaysOnClient(account, am.loadChannels(accountName), am.loadLastSeen(accountName), am.loadModes(accountName))
|
||||
am.server.AddAlwaysOnClient(
|
||||
account,
|
||||
am.loadChannels(accountName),
|
||||
am.loadLastSeen(accountName),
|
||||
am.loadModes(accountName),
|
||||
am.loadRealname(accountName),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,14 +381,14 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
|
||||
return errAccountCreation
|
||||
}
|
||||
|
||||
if restrictedCasefoldedNicks[casefoldedAccount] || restrictedSkeletons[skeleton] {
|
||||
if restrictedCasefoldedNicks.Has(casefoldedAccount) || restrictedSkeletons.Has(skeleton) {
|
||||
return errAccountAlreadyRegistered
|
||||
}
|
||||
|
||||
config := am.server.Config()
|
||||
|
||||
// final "is registration allowed" check, probably redundant:
|
||||
if !(config.Accounts.Registration.Enabled || callbackNamespace == "admin") {
|
||||
// final "is registration allowed" check:
|
||||
if !(config.Accounts.Registration.Enabled || callbackNamespace == "admin") || am.server.Defcon() <= 4 {
|
||||
return errFeatureDisabled
|
||||
}
|
||||
|
||||
@@ -617,11 +623,12 @@ func (am *AccountManager) loadModes(account string) (uModes modes.Modes) {
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) saveLastSeen(account string, lastSeen time.Time) {
|
||||
func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.Time) {
|
||||
key := fmt.Sprintf(keyAccountLastSeen, account)
|
||||
var val string
|
||||
if !lastSeen.IsZero() {
|
||||
val = strconv.FormatInt(lastSeen.UnixNano(), 10)
|
||||
if len(lastSeen) != 0 {
|
||||
text, _ := json.Marshal(lastSeen)
|
||||
val = string(text)
|
||||
}
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
if val != "" {
|
||||
@@ -633,21 +640,41 @@ func (am *AccountManager) saveLastSeen(account string, lastSeen time.Time) {
|
||||
})
|
||||
}
|
||||
|
||||
func (am *AccountManager) loadLastSeen(account string) (lastSeen time.Time) {
|
||||
func (am *AccountManager) loadLastSeen(account string) (lastSeen map[string]time.Time) {
|
||||
key := fmt.Sprintf(keyAccountLastSeen, account)
|
||||
var lsText string
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
lsText, _ = tx.Get(key)
|
||||
// XXX clear this on startup, because it's not clear when it's
|
||||
// going to be overwritten, and restarting the server twice in a row
|
||||
// could result in a large amount of duplicated history replay
|
||||
tx.Delete(key)
|
||||
return nil
|
||||
})
|
||||
lsNum, err := strconv.ParseInt(lsText, 10, 64)
|
||||
if err == nil {
|
||||
return time.Unix(0, lsNum).UTC()
|
||||
if lsText == "" {
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal([]byte(lsText), &lastSeen)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) saveRealname(account string, realname string) {
|
||||
key := fmt.Sprintf(keyAccountRealname, account)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
if realname != "" {
|
||||
tx.Set(key, realname, nil)
|
||||
} else {
|
||||
tx.Delete(key)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (am *AccountManager) loadRealname(account string) (realname string) {
|
||||
key := fmt.Sprintf(keyAccountRealname, account)
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
realname, _ = tx.Get(key)
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -864,14 +891,15 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
|
||||
}
|
||||
if client != nil {
|
||||
am.Login(client, clientAccount)
|
||||
}
|
||||
_, method := am.EnforcementStatus(casefoldedAccount, skeleton)
|
||||
if method != NickEnforcementNone {
|
||||
currentClient := am.server.clients.Get(casefoldedAccount)
|
||||
if currentClient == nil || currentClient == client || currentClient.Account() == casefoldedAccount {
|
||||
return nil
|
||||
if client.AlwaysOn() {
|
||||
client.markDirty(IncludeRealname)
|
||||
}
|
||||
if method == NickEnforcementStrict {
|
||||
}
|
||||
// we may need to do nick enforcement here:
|
||||
_, method := am.EnforcementStatus(casefoldedAccount, skeleton)
|
||||
if method == NickEnforcementStrict {
|
||||
currentClient := am.server.clients.Get(casefoldedAccount)
|
||||
if currentClient != nil && currentClient != client && currentClient.Account() != casefoldedAccount {
|
||||
am.server.RandomlyRename(currentClient)
|
||||
}
|
||||
}
|
||||
@@ -1052,6 +1080,10 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
|
||||
}
|
||||
}
|
||||
|
||||
if throttled, remainingTime := client.checkLoginThrottle(); throttled {
|
||||
return &ThrottleError{remainingTime}
|
||||
}
|
||||
|
||||
var account ClientAccount
|
||||
|
||||
defer func() {
|
||||
@@ -1061,24 +1093,13 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
|
||||
}()
|
||||
|
||||
config := am.server.Config()
|
||||
if config.Accounts.LDAP.Enabled {
|
||||
ldapConf := am.server.Config().Accounts.LDAP
|
||||
err = ldap.CheckLDAPPassphrase(ldapConf, accountName, passphrase, am.server.logger)
|
||||
if err != nil {
|
||||
account, err = am.loadWithAutocreation(accountName, ldapConf.Autocreate)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if config.Accounts.AuthScript.Enabled {
|
||||
var output AuthScriptOutput
|
||||
output, err = CheckAuthScript(config.Accounts.AuthScript,
|
||||
AuthScriptInput{AccountName: accountName, Passphrase: passphrase, IP: client.IP().String()})
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
||||
return err
|
||||
}
|
||||
if output.Success {
|
||||
} else if output.Success {
|
||||
if output.AccountName != "" {
|
||||
accountName = output.AccountName
|
||||
}
|
||||
@@ -1231,6 +1252,69 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
|
||||
return
|
||||
}
|
||||
|
||||
func (am *AccountManager) Suspend(accountName string) (err error) {
|
||||
account, err := CasefoldName(accountName)
|
||||
if err != nil {
|
||||
return errAccountDoesNotExist
|
||||
}
|
||||
|
||||
existsKey := fmt.Sprintf(keyAccountExists, account)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, account)
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
_, err := tx.Get(existsKey)
|
||||
if err != nil {
|
||||
return errAccountDoesNotExist
|
||||
}
|
||||
_, err = tx.Delete(verifiedKey)
|
||||
return err
|
||||
})
|
||||
|
||||
if err == errAccountDoesNotExist {
|
||||
return err
|
||||
} else if err != nil {
|
||||
am.server.logger.Error("internal", "couldn't persist suspension", account, err.Error())
|
||||
} // keep going
|
||||
|
||||
am.Lock()
|
||||
clients := am.accountToClients[account]
|
||||
delete(am.accountToClients, account)
|
||||
am.Unlock()
|
||||
|
||||
am.killClients(clients)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) killClients(clients []*Client) {
|
||||
for _, client := range clients {
|
||||
client.Logout()
|
||||
client.Quit(client.t("You are no longer authorized to be on this server"), nil)
|
||||
client.destroy(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AccountManager) Unsuspend(account string) (err error) {
|
||||
cfaccount, err := CasefoldName(account)
|
||||
if err != nil {
|
||||
return errAccountDoesNotExist
|
||||
}
|
||||
|
||||
existsKey := fmt.Sprintf(keyAccountExists, cfaccount)
|
||||
verifiedKey := fmt.Sprintf(keyAccountVerified, cfaccount)
|
||||
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
_, err := tx.Get(existsKey)
|
||||
if err != nil {
|
||||
return errAccountDoesNotExist
|
||||
}
|
||||
tx.Set(verifiedKey, "1", nil)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errAccountDoesNotExist
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
config := am.server.Config()
|
||||
casefoldedAccount, err := CasefoldName(account)
|
||||
@@ -1253,8 +1337,12 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount)
|
||||
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
||||
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
|
||||
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
|
||||
|
||||
var clients []*Client
|
||||
defer func() {
|
||||
am.killClients(clients)
|
||||
}()
|
||||
|
||||
var registeredChannels []string
|
||||
// on our way out, unregister all the account's channels and delete them from the db
|
||||
@@ -1307,6 +1395,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
tx.Delete(channelsKey)
|
||||
tx.Delete(joinedChannelsKey)
|
||||
tx.Delete(lastSeenKey)
|
||||
tx.Delete(modesKey)
|
||||
|
||||
_, err := tx.Delete(vhostQueueKey)
|
||||
am.decrementVHostQueueCount(casefoldedAccount, err)
|
||||
@@ -1347,12 +1436,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
additionalSkel, _ := Skeleton(nick)
|
||||
delete(am.skeletonToAccount, additionalSkel)
|
||||
}
|
||||
for _, client := range clients {
|
||||
client.Logout()
|
||||
client.Quit(client.t("You are no longer authorized to be on this server"), nil)
|
||||
// destroy acquires a semaphore so we can't call it while holding a lock
|
||||
go client.destroy(nil)
|
||||
}
|
||||
|
||||
if err != nil && !erase {
|
||||
return errAccountDoesNotExist
|
||||
@@ -1415,9 +1498,7 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client, certfp, authzid s
|
||||
AuthScriptInput{Certfp: certfp, IP: client.IP().String()})
|
||||
if err != nil {
|
||||
am.server.logger.Error("internal", "failed shell auth invocation", err.Error())
|
||||
return err
|
||||
}
|
||||
if output.Success && output.AccountName != "" {
|
||||
} else if output.Success && output.AccountName != "" {
|
||||
clientAccount, err = am.loadWithAutocreation(output.AccountName, config.Accounts.AuthScript.Autocreate)
|
||||
return
|
||||
}
|
||||
@@ -1492,7 +1573,6 @@ func (am *AccountManager) ModifyAccountSettings(account string, munger settingsM
|
||||
type VHostInfo struct {
|
||||
ApprovedVHost string
|
||||
Enabled bool
|
||||
Forbidden bool
|
||||
RequestedVHost string
|
||||
RejectedVHost string
|
||||
RejectionReason string
|
||||
@@ -1546,10 +1626,6 @@ func (am *AccountManager) VHostSet(account string, vhost string) (result VHostIn
|
||||
func (am *AccountManager) VHostRequest(account string, vhost string, cooldown time.Duration) (result VHostInfo, err error) {
|
||||
munger := func(input VHostInfo) (output VHostInfo, err error) {
|
||||
output = input
|
||||
if input.Forbidden {
|
||||
err = errVhostsForbidden
|
||||
return
|
||||
}
|
||||
// you can update your existing request, but if you were approved or rejected,
|
||||
// you can't spam a new request
|
||||
if output.RequestedVHost == "" {
|
||||
@@ -1568,32 +1644,6 @@ func (am *AccountManager) VHostRequest(account string, vhost string, cooldown ti
|
||||
return am.performVHostChange(account, munger)
|
||||
}
|
||||
|
||||
func (am *AccountManager) VHostTake(account string, vhost string, cooldown time.Duration) (result VHostInfo, err error) {
|
||||
munger := func(input VHostInfo) (output VHostInfo, err error) {
|
||||
output = input
|
||||
if input.Forbidden {
|
||||
err = errVhostsForbidden
|
||||
return
|
||||
}
|
||||
// if you have a request pending, you can cancel it using take;
|
||||
// otherwise, you're subject to the same throttling as if you were making a request
|
||||
if output.RequestedVHost == "" {
|
||||
err = output.checkThrottle(cooldown)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.ApprovedVHost = vhost
|
||||
output.RequestedVHost = ""
|
||||
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
|
||||
@@ -1633,16 +1683,6 @@ func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result
|
||||
return am.performVHostChange(client.Account(), munger)
|
||||
}
|
||||
|
||||
func (am *AccountManager) VHostForbid(account string, forbid bool) (result VHostInfo, err error) {
|
||||
munger := func(input VHostInfo) (output VHostInfo, err error) {
|
||||
output = input
|
||||
output.Forbidden = forbid
|
||||
return
|
||||
}
|
||||
|
||||
return am.performVHostChange(account, munger)
|
||||
}
|
||||
|
||||
func (am *AccountManager) performVHostChange(account string, munger vhostMunger) (result VHostInfo, err error) {
|
||||
account, err = CasefoldName(account)
|
||||
if err != nil || account == "" {
|
||||
@@ -1650,6 +1690,11 @@ func (am *AccountManager) performVHostChange(account string, munger vhostMunger)
|
||||
return
|
||||
}
|
||||
|
||||
if am.server.Defcon() <= 3 {
|
||||
err = errFeatureDisabled
|
||||
return
|
||||
}
|
||||
|
||||
am.vHostUpdateMutex.Lock()
|
||||
defer am.vHostUpdateMutex.Unlock()
|
||||
|
||||
@@ -1758,12 +1803,12 @@ func (am *AccountManager) applyVHostInfo(client *Client, info VHostInfo) {
|
||||
}
|
||||
|
||||
vhost := ""
|
||||
if info.Enabled && !info.Forbidden {
|
||||
if info.Enabled {
|
||||
vhost = info.ApprovedVHost
|
||||
}
|
||||
oldNickmask := client.NickMaskString()
|
||||
updated := client.SetVHost(vhost)
|
||||
if updated {
|
||||
if updated && client.Registered() {
|
||||
// TODO: doing I/O here is kind of a kludge
|
||||
client.sendChghost(oldNickmask, client.Hostname())
|
||||
}
|
||||
|
||||
+68
-71
@@ -6,7 +6,6 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -447,7 +446,7 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
|
||||
|
||||
maxNamLen := 480 - len(client.server.name) - len(client.Nick())
|
||||
var namesLines []string
|
||||
var buffer bytes.Buffer
|
||||
var buffer strings.Builder
|
||||
if isJoined || !channel.flags.HasMode(modes.Secret) || isOper {
|
||||
for _, target := range channel.Members() {
|
||||
var nick string
|
||||
@@ -540,6 +539,13 @@ func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) strin
|
||||
}
|
||||
}
|
||||
|
||||
func (channel *Channel) ClientStatus(client *Client) (present bool, cModes modes.Modes) {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
modes, present := channel.members[client]
|
||||
return present, modes.AllModes()
|
||||
}
|
||||
|
||||
func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
|
||||
channel.stateMutex.RLock()
|
||||
founder := channel.registeredFounder
|
||||
@@ -640,7 +646,7 @@ func channelHistoryStatus(config *Config, registered bool, storedStatus HistoryS
|
||||
}
|
||||
|
||||
func (channel *Channel) AddHistoryItem(item history.Item, account string) (err error) {
|
||||
if !item.IsStorable() {
|
||||
if !itemIsStorable(&item, channel.server.Config()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -654,7 +660,7 @@ func (channel *Channel) AddHistoryItem(item history.Item, account string) (err e
|
||||
}
|
||||
|
||||
// Join joins the given client to this channel (if they can be joined).
|
||||
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
|
||||
func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) error {
|
||||
details := client.Details()
|
||||
|
||||
channel.stateMutex.RLock()
|
||||
@@ -670,39 +676,44 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||
|
||||
if alreadyJoined {
|
||||
// no message needs to be sent
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// the founder can always join (even if they disabled auto +q on join);
|
||||
// anyone who automatically receives halfop or higher can always join
|
||||
hasPrivs := isSajoin || (founder != "" && founder == details.account) || (persistentMode != 0 && persistentMode != modes.Voice)
|
||||
// 0. SAJOIN always succeeds
|
||||
// 1. the founder can always join (even if they disabled auto +q on join)
|
||||
// 2. anyone who automatically receives halfop or higher can always join
|
||||
// 3. people invited with INVITE can join
|
||||
hasPrivs := isSajoin || (founder != "" && founder == details.account) ||
|
||||
(persistentMode != 0 && persistentMode != modes.Voice) ||
|
||||
client.CheckInvited(chcfname)
|
||||
if !hasPrivs {
|
||||
if limit != 0 && chcount >= limit {
|
||||
return errLimitExceeded
|
||||
}
|
||||
|
||||
if !hasPrivs && limit != 0 && chcount >= limit {
|
||||
rb.Add(nil, client.server.name, ERR_CHANNELISFULL, details.nick, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
|
||||
return
|
||||
if chkey != "" && !utils.SecretTokensMatch(chkey, key) {
|
||||
return errWrongChannelKey
|
||||
}
|
||||
|
||||
if channel.flags.HasMode(modes.InviteOnly) &&
|
||||
!channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded) {
|
||||
return errInviteOnly
|
||||
}
|
||||
|
||||
if channel.lists[modes.BanMask].Match(details.nickMaskCasefolded) &&
|
||||
!channel.lists[modes.ExceptMask].Match(details.nickMaskCasefolded) &&
|
||||
!channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded) {
|
||||
return errBanned
|
||||
}
|
||||
|
||||
if details.account == "" &&
|
||||
(channel.flags.HasMode(modes.RegisteredOnly) || channel.server.Defcon() <= 2) {
|
||||
return errRegisteredOnly
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPrivs && chkey != "" && !utils.SecretTokensMatch(chkey, key) {
|
||||
rb.Add(nil, client.server.name, ERR_BADCHANNELKEY, details.nick, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "k"))
|
||||
return
|
||||
}
|
||||
|
||||
isInvited := client.CheckInvited(chcfname) || channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded)
|
||||
if !hasPrivs && channel.flags.HasMode(modes.InviteOnly) && !isInvited {
|
||||
rb.Add(nil, client.server.name, ERR_INVITEONLYCHAN, details.nick, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "i"))
|
||||
return
|
||||
}
|
||||
|
||||
if !hasPrivs && channel.lists[modes.BanMask].Match(details.nickMaskCasefolded) &&
|
||||
!isInvited &&
|
||||
!channel.lists[modes.ExceptMask].Match(details.nickMaskCasefolded) {
|
||||
rb.Add(nil, client.server.name, ERR_BANNEDFROMCHAN, details.nick, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "b"))
|
||||
return
|
||||
}
|
||||
|
||||
if !hasPrivs && channel.flags.HasMode(modes.RegisteredOnly) && details.account == "" && !isInvited {
|
||||
rb.Add(nil, client.server.name, ERR_NEEDREGGEDNICK, details.nick, chname, client.t("You must be registered to join that channel"))
|
||||
return
|
||||
if joinErr := client.addChannel(channel, rb == nil); joinErr != nil {
|
||||
return joinErr
|
||||
}
|
||||
|
||||
client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
|
||||
@@ -747,10 +758,8 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||
channel.AddHistoryItem(histItem, details.account)
|
||||
}
|
||||
|
||||
client.addChannel(channel, rb == nil)
|
||||
|
||||
if rb == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
var modestr string
|
||||
@@ -758,6 +767,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||
modestr = fmt.Sprintf("+%v", givenMode)
|
||||
}
|
||||
|
||||
isAway, awayMessage := client.Away()
|
||||
for _, member := range channel.Members() {
|
||||
for _, session := range member.Sessions() {
|
||||
if session == rb.session {
|
||||
@@ -774,6 +784,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||
if givenMode != 0 {
|
||||
session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
|
||||
}
|
||||
if isAway && session.capabilities.Has(caps.AwayNotify) {
|
||||
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, nil, "AWAY", awayMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -793,22 +806,26 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||
rb.Flush(true)
|
||||
|
||||
channel.autoReplayHistory(client, rb, message.Msgid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
|
||||
// autoreplay any messages as necessary
|
||||
var items []history.Item
|
||||
|
||||
hasAutoreplayTimestamps := false
|
||||
var start, end time.Time
|
||||
if rb.session.zncPlaybackTimes.ValidFor(channel.NameCasefolded()) {
|
||||
hasAutoreplayTimestamps = true
|
||||
start, end = rb.session.zncPlaybackTimes.start, rb.session.zncPlaybackTimes.end
|
||||
} else if !rb.session.autoreplayMissedSince.IsZero() {
|
||||
// we already checked for history caps in `playReattachMessages`
|
||||
hasAutoreplayTimestamps = true
|
||||
start = time.Now().UTC()
|
||||
end = rb.session.autoreplayMissedSince
|
||||
}
|
||||
|
||||
if !start.IsZero() || !end.IsZero() {
|
||||
if hasAutoreplayTimestamps {
|
||||
_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
|
||||
if seq != nil {
|
||||
zncMax := channel.server.Config().History.ZNCMax
|
||||
@@ -1229,44 +1246,19 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
|
||||
}
|
||||
|
||||
// send echo-message
|
||||
if rb.session.capabilities.Has(caps.EchoMessage) {
|
||||
var tagsToUse map[string]string
|
||||
if rb.session.capabilities.Has(caps.MessageTags) {
|
||||
tagsToUse = clientOnlyTags
|
||||
}
|
||||
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
|
||||
rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, tagsToUse, command, chname)
|
||||
} else {
|
||||
rb.AddSplitMessageFromClient(details.nickMask, details.accountName, tagsToUse, command, chname, message)
|
||||
}
|
||||
}
|
||||
// send echo-message to other connected sessions
|
||||
for _, session := range client.Sessions() {
|
||||
if session == rb.session {
|
||||
continue
|
||||
}
|
||||
var tagsToUse map[string]string
|
||||
if session.capabilities.Has(caps.MessageTags) {
|
||||
tagsToUse = clientOnlyTags
|
||||
}
|
||||
if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) {
|
||||
session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, tagsToUse, command, chname)
|
||||
} else if histType != history.Tagmsg {
|
||||
session.sendSplitMsgFromClientInternal(false, details.nickMask, details.accountName, tagsToUse, command, chname, message)
|
||||
}
|
||||
}
|
||||
rb.addEchoMessage(clientOnlyTags, details.nickMask, details.accountName, command, chname, message)
|
||||
|
||||
for _, member := range channel.Members() {
|
||||
// echo-message is handled above, so skip sending the msg to the user themselves as well
|
||||
if member == client {
|
||||
continue
|
||||
}
|
||||
if minPrefixMode != modes.Mode(0) && !channel.ClientIsAtLeast(member, minPrefixMode) {
|
||||
// STATUSMSG
|
||||
continue
|
||||
}
|
||||
|
||||
for _, session := range member.Sessions() {
|
||||
if session == rb.session {
|
||||
continue // we already sent echo-message, if applicable
|
||||
}
|
||||
|
||||
if isCTCP && session.isTor {
|
||||
continue // #753
|
||||
}
|
||||
@@ -1431,9 +1423,7 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
|
||||
return
|
||||
}
|
||||
|
||||
if channel.flags.HasMode(modes.InviteOnly) {
|
||||
invitee.Invite(channel.NameCasefolded())
|
||||
}
|
||||
invitee.Invite(channel.NameCasefolded())
|
||||
|
||||
for _, member := range channel.Members() {
|
||||
if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) {
|
||||
@@ -1450,7 +1440,14 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
|
||||
tnick := invitee.Nick()
|
||||
rb.Add(nil, inviter.server.name, RPL_INVITING, cnick, tnick, chname)
|
||||
invitee.Send(nil, inviter.NickMaskString(), "INVITE", tnick, chname)
|
||||
if invitee.Away() {
|
||||
rb.Add(nil, inviter.server.name, RPL_AWAY, cnick, tnick, invitee.AwayMessage())
|
||||
if away, awayMessage := invitee.Away(); away {
|
||||
rb.Add(nil, inviter.server.name, RPL_AWAY, cnick, tnick, awayMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// data for RPL_LIST
|
||||
func (channel *Channel) listData() (memberCount int, name, topic string) {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
return len(channel.members), channel.name, channel.topic
|
||||
}
|
||||
|
||||
+23
-9
@@ -5,6 +5,8 @@ package irc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
type channelManagerEntry struct {
|
||||
@@ -23,17 +25,17 @@ type ChannelManager struct {
|
||||
sync.RWMutex // tier 2
|
||||
// chans is the main data structure, mapping casefolded name -> *Channel
|
||||
chans map[string]*channelManagerEntry
|
||||
chansSkeletons StringSet // skeletons of *unregistered* chans
|
||||
registeredChannels StringSet // casefolds of registered chans
|
||||
registeredSkeletons StringSet // skeletons of registered chans
|
||||
purgedChannels StringSet // casefolds of purged chans
|
||||
chansSkeletons utils.StringSet // skeletons of *unregistered* chans
|
||||
registeredChannels utils.StringSet // casefolds of registered chans
|
||||
registeredSkeletons utils.StringSet // skeletons of registered chans
|
||||
purgedChannels utils.StringSet // casefolds of purged chans
|
||||
server *Server
|
||||
}
|
||||
|
||||
// NewChannelManager returns a new ChannelManager.
|
||||
func (cm *ChannelManager) Initialize(server *Server) {
|
||||
cm.chans = make(map[string]*channelManagerEntry)
|
||||
cm.chansSkeletons = make(StringSet)
|
||||
cm.chansSkeletons = make(utils.StringSet)
|
||||
cm.server = server
|
||||
|
||||
cm.loadRegisteredChannels(server.Config())
|
||||
@@ -47,8 +49,8 @@ func (cm *ChannelManager) loadRegisteredChannels(config *Config) {
|
||||
}
|
||||
|
||||
rawNames := cm.server.channelRegistry.AllChannels()
|
||||
registeredChannels := make(StringSet, len(rawNames))
|
||||
registeredSkeletons := make(StringSet, len(rawNames))
|
||||
registeredChannels := make(utils.StringSet, len(rawNames))
|
||||
registeredSkeletons := make(utils.StringSet, len(rawNames))
|
||||
for _, name := range rawNames {
|
||||
cfname, err := CasefoldChannel(name)
|
||||
if err == nil {
|
||||
@@ -130,11 +132,11 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
|
||||
}
|
||||
|
||||
channel.EnsureLoaded()
|
||||
channel.Join(client, key, isSajoin, rb)
|
||||
err = channel.Join(client, key, isSajoin, rb)
|
||||
|
||||
cm.maybeCleanup(channel, true)
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {
|
||||
@@ -187,6 +189,10 @@ func (cm *ChannelManager) Cleanup(channel *Channel) {
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) {
|
||||
if cm.server.Defcon() <= 4 {
|
||||
return errFeatureDisabled
|
||||
}
|
||||
|
||||
var channel *Channel
|
||||
cfname, err := CasefoldChannel(channelName)
|
||||
if err != nil {
|
||||
@@ -283,6 +289,14 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||
cm.Lock()
|
||||
defer cm.Unlock()
|
||||
|
||||
if newCfname == cfname {
|
||||
entry := cm.chans[cfname]
|
||||
if entry == nil || !entry.channel.IsLoaded() {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
entry.channel.Rename(newName, cfname)
|
||||
return nil
|
||||
}
|
||||
if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) {
|
||||
return errChannelNameInUse
|
||||
}
|
||||
|
||||
+6
-5
@@ -4,15 +4,16 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"encoding/json"
|
||||
"github.com/tidwall/buntdb"
|
||||
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/tidwall/buntdb"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
// this is exclusively the *persistence* layer for channel registration;
|
||||
@@ -140,8 +141,8 @@ func (reg *ChannelRegistry) AllChannels() (result []string) {
|
||||
}
|
||||
|
||||
// PurgedChannels returns the set of all casefolded channel names that have been purged
|
||||
func (reg *ChannelRegistry) PurgedChannels() (result map[string]empty) {
|
||||
result = make(map[string]empty)
|
||||
func (reg *ChannelRegistry) PurgedChannels() (result utils.StringSet) {
|
||||
result = make(utils.StringSet)
|
||||
|
||||
prefix := fmt.Sprintf(keyChannelPurged, "")
|
||||
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||
@@ -150,7 +151,7 @@ func (reg *ChannelRegistry) PurgedChannels() (result map[string]empty) {
|
||||
return false
|
||||
}
|
||||
channel := strings.TrimPrefix(key, prefix)
|
||||
result[channel] = empty{}
|
||||
result.Add(channel)
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
+1
-1
@@ -249,7 +249,7 @@ func csAmodeHandler(server *Server, client *Client, command string, params []str
|
||||
}
|
||||
case modes.Add, modes.Remove:
|
||||
if len(affectedModes) > 0 {
|
||||
csNotice(rb, fmt.Sprintf(client.t("Successfully set persistent mode %s%s on %s"), string(change.Op), string(change.Mode), change.Arg))
|
||||
csNotice(rb, fmt.Sprintf(client.t("Successfully set persistent mode %[1]s on %[2]s"), strings.Join([]string{string(change.Op), string(change.Mode)}, ""), change.Arg))
|
||||
// #729: apply change to current membership
|
||||
for _, member := range channel.Members() {
|
||||
if member.Account() == change.Arg {
|
||||
|
||||
+292
-82
@@ -27,9 +27,36 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// maximum line length not including tags; don't change this for a public server
|
||||
MaxLineLen = 512
|
||||
|
||||
// IdentTimeout is how long before our ident (username) check times out.
|
||||
IdentTimeout = time.Second + 500*time.Millisecond
|
||||
IRCv3TimestampFormat = utils.IRCv3TimestampFormat
|
||||
// limit the number of device IDs a client can use, as a DoS mitigation
|
||||
maxDeviceIDsPerClient = 64
|
||||
// controls how often often we write an autoreplay-missed client's
|
||||
// deviceid->lastseentime mapping to the database
|
||||
lastSeenWriteInterval = time.Hour
|
||||
)
|
||||
|
||||
const (
|
||||
// RegisterTimeout is how long clients have to register before we disconnect them
|
||||
RegisterTimeout = time.Minute
|
||||
// DefaultIdleTimeout is how long without traffic before we send the client a PING
|
||||
DefaultIdleTimeout = time.Minute + 30*time.Second
|
||||
// For Tor clients, we send a PING at least every 30 seconds, as a workaround for this bug
|
||||
// (single-onion circuits will close unless the client sends data once every 60 seconds):
|
||||
// https://bugs.torproject.org/29665
|
||||
TorIdleTimeout = time.Second * 30
|
||||
// This is how long a client gets without sending any message, including the PONG to our
|
||||
// PING, before we disconnect them:
|
||||
DefaultTotalTimeout = 2*time.Minute + 30*time.Second
|
||||
// Resumeable clients (clients who have negotiated caps.Resume) get longer:
|
||||
ResumeableTotalTimeout = 3*time.Minute + 30*time.Second
|
||||
|
||||
// round off the ping interval by this much, see below:
|
||||
PingCoalesceThreshold = time.Second
|
||||
)
|
||||
|
||||
// ResumeDetails is a place to stash data at various stages of
|
||||
@@ -54,14 +81,14 @@ type Client struct {
|
||||
channels ChannelSet
|
||||
ctime time.Time
|
||||
destroyed bool
|
||||
exitedSnomaskSent bool
|
||||
modes modes.ModeSet
|
||||
hostname string
|
||||
invitedTo map[string]bool
|
||||
invitedTo utils.StringSet
|
||||
isSTSOnly bool
|
||||
languages []string
|
||||
lastActive time.Time // last time they sent a command that wasn't PONG or similar
|
||||
lastSeen time.Time // last time they sent any kind of command
|
||||
lastActive time.Time // last time they sent a command that wasn't PONG or similar
|
||||
lastSeen map[string]time.Time // maps device ID (including "") to time of last received command
|
||||
lastSeenLastWrite time.Time // last time `lastSeen` was written to the datastore
|
||||
loginThrottle connection_limits.GenericThrottle
|
||||
nick string
|
||||
nickCasefolded string
|
||||
@@ -75,6 +102,7 @@ type Client struct {
|
||||
realname string
|
||||
realIP net.IP
|
||||
registered bool
|
||||
registrationTimer *time.Timer
|
||||
resumeID string
|
||||
server *Server
|
||||
skeleton string
|
||||
@@ -112,8 +140,13 @@ const (
|
||||
type Session struct {
|
||||
client *Client
|
||||
|
||||
deviceID string
|
||||
|
||||
ctime time.Time
|
||||
lastActive time.Time
|
||||
lastActive time.Time // last non-CTCP PRIVMSG sent; updates publicly visible idle time
|
||||
lastTouch time.Time // last line sent; updates timer for idle timeouts
|
||||
idleTimer *time.Timer
|
||||
pingSent bool // we sent PING to a putatively idle connection and we're waiting for PONG
|
||||
|
||||
socket *Socket
|
||||
realIP net.IP
|
||||
@@ -121,7 +154,6 @@ type Session struct {
|
||||
rawHostname string
|
||||
isTor bool
|
||||
|
||||
idletimer IdleTimer
|
||||
fakelag Fakelag
|
||||
deferredFakelagCount int
|
||||
destroyed uint32
|
||||
@@ -178,8 +210,8 @@ func (s *Session) EndMultilineBatch(label string) (batch MultilineBatch, err err
|
||||
s.fakelag.Unsuspend()
|
||||
|
||||
// heuristics to estimate how much data they used while fakelag was suspended
|
||||
fakelagBill := (batch.lenBytes / 512) + 1
|
||||
fakelagBillLines := (batch.message.LenLines() * 60) / 512
|
||||
fakelagBill := (batch.lenBytes / MaxLineLen) + 1
|
||||
fakelagBillLines := (batch.message.LenLines() * 60) / MaxLineLen
|
||||
if fakelagBill < fakelagBillLines {
|
||||
fakelagBill = fakelagBillLines
|
||||
}
|
||||
@@ -299,7 +331,6 @@ func (server *Server) RunClient(conn IRCConn) {
|
||||
// give them 1k of grace over the limit:
|
||||
socket := NewSocket(conn, config.Server.MaxSendQBytes)
|
||||
client := &Client{
|
||||
lastSeen: now,
|
||||
lastActive: now,
|
||||
channels: make(ChannelSet),
|
||||
ctime: now,
|
||||
@@ -333,7 +364,6 @@ func (server *Server) RunClient(conn IRCConn) {
|
||||
}
|
||||
client.sessions = []*Session{session}
|
||||
|
||||
session.idletimer.Initialize(session)
|
||||
session.resetFakelag()
|
||||
|
||||
if wConn.Secure {
|
||||
@@ -354,15 +384,16 @@ func (server *Server) RunClient(conn IRCConn) {
|
||||
}
|
||||
}
|
||||
|
||||
client.registrationTimer = time.AfterFunc(RegisterTimeout, client.handleRegisterTimeout)
|
||||
server.stats.Add()
|
||||
client.run(session)
|
||||
}
|
||||
|
||||
func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSeen time.Time, uModes modes.Modes) {
|
||||
func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSeen map[string]time.Time, uModes modes.Modes, realname string) {
|
||||
now := time.Now().UTC()
|
||||
config := server.Config()
|
||||
if lastSeen.IsZero() {
|
||||
lastSeen = now
|
||||
if lastSeen == nil && account.Settings.AutoreplayMissed {
|
||||
lastSeen = map[string]time.Time{"": now}
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
@@ -379,6 +410,7 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string,
|
||||
realIP: utils.IPv4LoopbackAddress,
|
||||
|
||||
alwaysOn: true,
|
||||
realname: realname,
|
||||
}
|
||||
|
||||
client.SetMode(modes.TLS, true)
|
||||
@@ -522,7 +554,7 @@ const (
|
||||
authFailSaslRequired
|
||||
)
|
||||
|
||||
func (client *Client) isAuthorized(config *Config, session *Session) AuthOutcome {
|
||||
func (client *Client) isAuthorized(server *Server, config *Config, session *Session) AuthOutcome {
|
||||
saslSent := client.account != ""
|
||||
// PASS requirement
|
||||
if (config.Server.passwordBytes != nil) && session.passStatus != serverPassSuccessful && !(config.Accounts.SkipServerPassword && saslSent) {
|
||||
@@ -533,7 +565,8 @@ func (client *Client) isAuthorized(config *Config, session *Session) AuthOutcome
|
||||
return authFailTorSaslRequired
|
||||
}
|
||||
// finally, enforce require-sasl
|
||||
if config.Accounts.RequireSasl.Enabled && !saslSent && !utils.IPInNets(session.IP(), config.Accounts.RequireSasl.exemptedNets) {
|
||||
if !saslSent && (config.Accounts.RequireSasl.Enabled || server.Defcon() <= 2) &&
|
||||
!utils.IPInNets(session.IP(), config.Accounts.RequireSasl.exemptedNets) {
|
||||
return authFailSaslRequired
|
||||
}
|
||||
return authSuccess
|
||||
@@ -594,7 +627,7 @@ func (client *Client) run(session *Session) {
|
||||
|
||||
isReattach := client.Registered()
|
||||
if isReattach {
|
||||
session.idletimer.Touch()
|
||||
client.Touch(session)
|
||||
if session.resumeDetails != nil {
|
||||
session.playResume()
|
||||
session.resumeDetails = nil
|
||||
@@ -608,8 +641,11 @@ func (client *Client) run(session *Session) {
|
||||
firstLine := !isReattach
|
||||
|
||||
for {
|
||||
var invalidUtf8 bool
|
||||
line, err := session.socket.Read()
|
||||
if err != nil {
|
||||
if err == errInvalidUtf8 {
|
||||
invalidUtf8 = true // handle as normal, including labeling
|
||||
} else if err != nil {
|
||||
quitMessage := "connection closed"
|
||||
if err == errReadQ {
|
||||
quitMessage = "readQ exceeded"
|
||||
@@ -655,7 +691,7 @@ func (client *Client) run(session *Session) {
|
||||
}
|
||||
}
|
||||
|
||||
msg, err := ircmsg.ParseLineStrict(line, true, 512)
|
||||
msg, err := ircmsg.ParseLineStrict(line, true, MaxLineLen)
|
||||
if err == ircmsg.ErrorLineIsEmpty {
|
||||
continue
|
||||
} else if err == ircmsg.ErrorLineTooLong {
|
||||
@@ -669,6 +705,8 @@ func (client *Client) run(session *Session) {
|
||||
cmd, exists := Commands[msg.Command]
|
||||
if !exists {
|
||||
cmd = unknownCommand
|
||||
} else if invalidUtf8 {
|
||||
cmd = invalidUtf8Command
|
||||
}
|
||||
|
||||
isExiting := cmd.Run(client.server, client, session, msg)
|
||||
@@ -711,16 +749,109 @@ func (client *Client) playReattachMessages(session *Session) {
|
||||
//
|
||||
|
||||
// Touch indicates that we received a line from the client (so the connection is healthy
|
||||
// at this time, modulo network latency and fakelag). `active` means not a PING or suchlike
|
||||
// (i.e. the user should be sitting in front of their client).
|
||||
func (client *Client) Touch(active bool, session *Session) {
|
||||
// at this time, modulo network latency and fakelag).
|
||||
func (client *Client) Touch(session *Session) {
|
||||
var markDirty bool
|
||||
now := time.Now().UTC()
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
client.lastSeen = now
|
||||
if active {
|
||||
client.lastActive = now
|
||||
session.lastActive = now
|
||||
if client.accountSettings.AutoreplayMissed || session.deviceID != "" {
|
||||
client.setLastSeen(now, session.deviceID)
|
||||
if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
|
||||
markDirty = true
|
||||
client.lastSeenLastWrite = now
|
||||
}
|
||||
}
|
||||
client.updateIdleTimer(session, now)
|
||||
client.stateMutex.Unlock()
|
||||
if markDirty {
|
||||
client.markDirty(IncludeLastSeen)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) setLastSeen(now time.Time, deviceID string) {
|
||||
if client.lastSeen == nil {
|
||||
client.lastSeen = make(map[string]time.Time)
|
||||
}
|
||||
client.lastSeen[deviceID] = now
|
||||
// evict the least-recently-used entry if necessary
|
||||
if maxDeviceIDsPerClient < len(client.lastSeen) {
|
||||
var minLastSeen time.Time
|
||||
var minClientId string
|
||||
for deviceID, lastSeen := range client.lastSeen {
|
||||
if minLastSeen.IsZero() || lastSeen.Before(minLastSeen) {
|
||||
minClientId, minLastSeen = deviceID, lastSeen
|
||||
}
|
||||
}
|
||||
delete(client.lastSeen, minClientId)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) updateIdleTimer(session *Session, now time.Time) {
|
||||
session.lastTouch = now
|
||||
session.pingSent = false
|
||||
|
||||
if session.idleTimer == nil {
|
||||
pingTimeout := DefaultIdleTimeout
|
||||
if session.isTor {
|
||||
pingTimeout = TorIdleTimeout
|
||||
}
|
||||
session.idleTimer = time.AfterFunc(pingTimeout, session.handleIdleTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (session *Session) handleIdleTimeout() {
|
||||
totalTimeout := DefaultTotalTimeout
|
||||
if session.capabilities.Has(caps.Resume) {
|
||||
totalTimeout = ResumeableTotalTimeout
|
||||
}
|
||||
pingTimeout := DefaultIdleTimeout
|
||||
if session.isTor {
|
||||
pingTimeout = TorIdleTimeout
|
||||
}
|
||||
|
||||
session.client.stateMutex.Lock()
|
||||
now := time.Now()
|
||||
timeUntilDestroy := session.lastTouch.Add(totalTimeout).Sub(now)
|
||||
timeUntilPing := session.lastTouch.Add(pingTimeout).Sub(now)
|
||||
shouldDestroy := session.pingSent && timeUntilDestroy <= 0
|
||||
// XXX this should really be time <= 0, but let's do some hacky timer coalescing:
|
||||
// a typical idling client will do nothing other than respond immediately to our pings,
|
||||
// so we'll PING at t=0, they'll respond at t=0.05, then we'll wake up at t=90 and find
|
||||
// that we need to PING again at t=90.05. Rather than wake up again, just send it now:
|
||||
shouldSendPing := !session.pingSent && timeUntilPing <= PingCoalesceThreshold
|
||||
if !shouldDestroy {
|
||||
if shouldSendPing {
|
||||
session.pingSent = true
|
||||
}
|
||||
// check in again at the minimum of these 3 possible intervals:
|
||||
// 1. the ping timeout (assuming we PING and they reply immediately with PONG)
|
||||
// 2. the next time we would send PING (if they don't send any more lines)
|
||||
// 3. the next time we would destroy (if they don't send any more lines)
|
||||
nextTimeout := pingTimeout
|
||||
if PingCoalesceThreshold < timeUntilPing && timeUntilPing < nextTimeout {
|
||||
nextTimeout = timeUntilPing
|
||||
}
|
||||
if 0 < timeUntilDestroy && timeUntilDestroy < nextTimeout {
|
||||
nextTimeout = timeUntilDestroy
|
||||
}
|
||||
session.idleTimer.Stop()
|
||||
session.idleTimer.Reset(nextTimeout)
|
||||
}
|
||||
session.client.stateMutex.Unlock()
|
||||
|
||||
if shouldDestroy {
|
||||
session.client.Quit(fmt.Sprintf("Ping timeout: %v", totalTimeout), session)
|
||||
session.client.destroy(session)
|
||||
} else if shouldSendPing {
|
||||
session.Ping()
|
||||
}
|
||||
}
|
||||
|
||||
func (session *Session) stopIdleTimer() {
|
||||
session.client.stateMutex.Lock()
|
||||
defer session.client.stateMutex.Unlock()
|
||||
if session.idleTimer != nil {
|
||||
session.idleTimer.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,23 +1081,10 @@ func (client *Client) IdleSeconds() uint64 {
|
||||
return uint64(client.IdleTime().Seconds())
|
||||
}
|
||||
|
||||
// HasNick returns true if the client's nickname is set (used in registration).
|
||||
func (client *Client) HasNick() bool {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.nick != "" && client.nick != "*"
|
||||
}
|
||||
|
||||
// HasUsername returns true if the client's username is set (used in registration).
|
||||
func (client *Client) HasUsername() bool {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.username != "" && client.username != "*"
|
||||
}
|
||||
|
||||
// SetNames sets the client's ident and realname.
|
||||
func (client *Client) SetNames(username, realname string, fromIdent bool) error {
|
||||
limit := client.server.Config().Limits.IdentLen
|
||||
config := client.server.Config()
|
||||
limit := config.Limits.IdentLen
|
||||
if !fromIdent {
|
||||
limit -= 1 // leave room for the prepended ~
|
||||
}
|
||||
@@ -978,7 +1096,9 @@ func (client *Client) SetNames(username, realname string, fromIdent bool) error
|
||||
return errInvalidUsername
|
||||
}
|
||||
|
||||
if !fromIdent {
|
||||
if config.Server.SuppressIdent {
|
||||
username = "~user"
|
||||
} else if !fromIdent {
|
||||
username = "~" + username
|
||||
}
|
||||
|
||||
@@ -1018,29 +1138,32 @@ func (client *Client) ModeString() (str string) {
|
||||
}
|
||||
|
||||
// Friends refers to clients that share a channel with this client.
|
||||
func (client *Client) Friends(capabs ...caps.Capability) (result map[*Session]bool) {
|
||||
result = make(map[*Session]bool)
|
||||
func (client *Client) Friends(capabs ...caps.Capability) (result map[*Session]empty) {
|
||||
result = make(map[*Session]empty)
|
||||
|
||||
// look at the client's own sessions
|
||||
for _, session := range client.Sessions() {
|
||||
if session.capabilities.HasAll(capabs...) {
|
||||
result[session] = true
|
||||
}
|
||||
}
|
||||
addFriendsToSet(result, client, capabs...)
|
||||
|
||||
for _, channel := range client.Channels() {
|
||||
for _, member := range channel.Members() {
|
||||
for _, session := range member.Sessions() {
|
||||
if session.capabilities.HasAll(capabs...) {
|
||||
result[session] = true
|
||||
}
|
||||
}
|
||||
addFriendsToSet(result, member, capabs...)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// helper for Friends
|
||||
func addFriendsToSet(set map[*Session]empty, client *Client, capabs ...caps.Capability) {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
for _, session := range client.sessions {
|
||||
if session.capabilities.HasAll(capabs...) {
|
||||
set[session] = empty{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) SetOper(oper *Oper) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
@@ -1082,14 +1205,25 @@ func (client *Client) SetVHost(vhost string) (updated bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// updateNick updates `nick` and `nickCasefolded`.
|
||||
func (client *Client) updateNick(nick, nickCasefolded, skeleton string) {
|
||||
// SetNick gives the client a nickname and marks it as registered, if necessary
|
||||
func (client *Client) SetNick(nick, nickCasefolded, skeleton string) (success bool) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
if client.destroyed {
|
||||
return false
|
||||
} else if !client.registered {
|
||||
// XXX test this before setting it to avoid annoying the race detector
|
||||
client.registered = true
|
||||
if client.registrationTimer != nil {
|
||||
client.registrationTimer.Stop()
|
||||
client.registrationTimer = nil
|
||||
}
|
||||
}
|
||||
client.nick = nick
|
||||
client.nickCasefolded = nickCasefolded
|
||||
client.skeleton = skeleton
|
||||
client.updateNickMaskNoMutex()
|
||||
return true
|
||||
}
|
||||
|
||||
// updateNickMaskNoMutex updates the casefolded nickname and nickmask, not acquiring any mutexes.
|
||||
@@ -1159,11 +1293,11 @@ func (client *Client) Quit(message string, session *Session) {
|
||||
// #364: don't send QUIT lines to unregistered clients
|
||||
if client.registered {
|
||||
quitMsg := ircmsg.MakeMessage(nil, client.nickMaskString, "QUIT", message)
|
||||
finalData, _ = quitMsg.LineBytesStrict(false, 512)
|
||||
finalData, _ = quitMsg.LineBytesStrict(false, MaxLineLen)
|
||||
}
|
||||
|
||||
errorMsg := ircmsg.MakeMessage(nil, "", "ERROR", message)
|
||||
errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512)
|
||||
errorMsgBytes, _ := errorMsg.LineBytesStrict(false, MaxLineLen)
|
||||
finalData = append(finalData, errorMsgBytes...)
|
||||
|
||||
sess.socket.SetFinalData(finalData)
|
||||
@@ -1193,16 +1327,20 @@ func (client *Client) Quit(message string, session *Session) {
|
||||
func (client *Client) destroy(session *Session) {
|
||||
config := client.server.Config()
|
||||
var sessionsToDestroy []*Session
|
||||
var saveLastSeen bool
|
||||
|
||||
client.stateMutex.Lock()
|
||||
|
||||
details := client.detailsNoMutex()
|
||||
brbState := client.brbTimer.state
|
||||
brbAt := client.brbTimer.brbAt
|
||||
wasReattach := session != nil && session.client != client
|
||||
sessionRemoved := false
|
||||
registered := client.registered
|
||||
alwaysOn := client.alwaysOn
|
||||
saveLastSeen := alwaysOn && client.accountSettings.AutoreplayMissed
|
||||
// XXX a temporary (reattaching) client can be marked alwaysOn when it logs in,
|
||||
// but then the session attaches to another client and we need to clean it up here
|
||||
alwaysOn := registered && client.alwaysOn
|
||||
|
||||
var remainingSessions int
|
||||
if session == nil {
|
||||
sessionsToDestroy = client.sessions
|
||||
@@ -1215,6 +1353,20 @@ func (client *Client) destroy(session *Session) {
|
||||
}
|
||||
}
|
||||
|
||||
// save last seen if applicable:
|
||||
if alwaysOn {
|
||||
if client.accountSettings.AutoreplayMissed {
|
||||
saveLastSeen = true
|
||||
} else {
|
||||
for _, session := range sessionsToDestroy {
|
||||
if session.deviceID != "" {
|
||||
saveLastSeen = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// should we destroy the whole client this time?
|
||||
// BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
|
||||
brbEligible := session != nil && brbState == BrbEnabled
|
||||
@@ -1229,18 +1381,23 @@ func (client *Client) destroy(session *Session) {
|
||||
if saveLastSeen {
|
||||
client.dirtyBits |= IncludeLastSeen
|
||||
}
|
||||
exitedSnomaskSent := client.exitedSnomaskSent
|
||||
|
||||
autoAway := false
|
||||
var awayMessage string
|
||||
if alwaysOn && remainingSessions == 0 && persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) {
|
||||
if alwaysOn && !client.away && remainingSessions == 0 &&
|
||||
persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) {
|
||||
autoAway = true
|
||||
client.autoAway = true
|
||||
client.away = true
|
||||
awayMessage = config.languageManager.Translate(client.languages, `Disconnected from the server`)
|
||||
awayMessage = config.languageManager.Translate(client.languages, `User is currently disconnected`)
|
||||
client.awayMessage = awayMessage
|
||||
}
|
||||
|
||||
if client.registrationTimer != nil {
|
||||
// unconditionally stop; if the client is still unregistered it must be destroyed
|
||||
client.registrationTimer.Stop()
|
||||
}
|
||||
|
||||
client.stateMutex.Unlock()
|
||||
|
||||
// XXX there is no particular reason to persist this state here rather than
|
||||
@@ -1258,13 +1415,16 @@ func (client *Client) destroy(session *Session) {
|
||||
// session has been attached to a new client; do not destroy it
|
||||
continue
|
||||
}
|
||||
session.idletimer.Stop()
|
||||
session.stopIdleTimer()
|
||||
// send quit/error message to client if they haven't been sent already
|
||||
client.Quit("", session)
|
||||
quitMessage = session.quitMessage
|
||||
session.SetDestroyed()
|
||||
session.socket.Close()
|
||||
|
||||
// clean up monitor state
|
||||
client.server.monitorManager.RemoveAll(session)
|
||||
|
||||
// remove from connection limits
|
||||
var source string
|
||||
if session.isTor {
|
||||
@@ -1330,8 +1490,6 @@ func (client *Client) destroy(session *Session) {
|
||||
if registered {
|
||||
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false)
|
||||
}
|
||||
// clean up monitor state
|
||||
client.server.monitorManager.RemoveAll(client)
|
||||
|
||||
// clean up channels
|
||||
// (note that if this is a reattach, client has no channels and therefore no friends)
|
||||
@@ -1370,7 +1528,7 @@ func (client *Client) destroy(session *Session) {
|
||||
friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
|
||||
}
|
||||
|
||||
if !exitedSnomaskSent && registered {
|
||||
if registered {
|
||||
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick))
|
||||
}
|
||||
}
|
||||
@@ -1478,10 +1636,11 @@ func (session *Session) SendRawMessage(message ircmsg.IrcMessage, blocking bool)
|
||||
}
|
||||
|
||||
// assemble message
|
||||
line, err := message.LineBytesStrict(false, 512)
|
||||
line, err := message.LineBytesStrict(false, MaxLineLen)
|
||||
if err != nil {
|
||||
logline := fmt.Sprintf("Error assembling message for sending: %v\n%s", err, debug.Stack())
|
||||
session.client.server.logger.Error("internal", logline)
|
||||
errorParams := []string{"Error assembling message for sending", err.Error(), message.Command}
|
||||
errorParams = append(errorParams, message.Params...)
|
||||
session.client.server.logger.Error("internal", errorParams...)
|
||||
|
||||
message = ircmsg.MakeMessage(nil, session.client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
|
||||
line, _ := message.LineBytesStrict(false, 0)
|
||||
@@ -1543,15 +1702,24 @@ func (session *Session) Notice(text string) {
|
||||
|
||||
// `simulated` is for the fake join of an always-on client
|
||||
// (we just read the channel name from the database, there's no need to write it back)
|
||||
func (client *Client) addChannel(channel *Channel, simulated bool) {
|
||||
func (client *Client) addChannel(channel *Channel, simulated bool) (err error) {
|
||||
config := client.server.Config()
|
||||
|
||||
client.stateMutex.Lock()
|
||||
client.channels[channel] = true
|
||||
alwaysOn := client.alwaysOn
|
||||
if client.destroyed {
|
||||
err = errClientDestroyed
|
||||
} else if client.oper == nil && len(client.channels) >= config.Channels.MaxChannelsPerClient {
|
||||
err = errTooManyChannels
|
||||
} else {
|
||||
client.channels[channel] = empty{} // success
|
||||
}
|
||||
client.stateMutex.Unlock()
|
||||
|
||||
if alwaysOn && !simulated {
|
||||
if err == nil && alwaysOn && !simulated {
|
||||
client.markDirty(IncludeChannels)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) removeChannel(channel *Channel) {
|
||||
@@ -1571,10 +1739,10 @@ func (client *Client) Invite(casefoldedChannel string) {
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
if client.invitedTo == nil {
|
||||
client.invitedTo = make(map[string]bool)
|
||||
client.invitedTo = make(utils.StringSet)
|
||||
}
|
||||
|
||||
client.invitedTo[casefoldedChannel] = true
|
||||
client.invitedTo.Add(casefoldedChannel)
|
||||
}
|
||||
|
||||
// Checks that the client was invited to join a given channel
|
||||
@@ -1582,7 +1750,7 @@ func (client *Client) CheckInvited(casefoldedChannel string) (invited bool) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
|
||||
invited = client.invitedTo[casefoldedChannel]
|
||||
invited = client.invitedTo.Has(casefoldedChannel)
|
||||
// joining an invited channel "uses up" your invite, so you can't rejoin on kick
|
||||
delete(client.invitedTo, casefoldedChannel)
|
||||
return
|
||||
@@ -1595,7 +1763,7 @@ func (client *Client) attemptAutoOper(session *Session) {
|
||||
return
|
||||
}
|
||||
for _, oper := range client.server.Config().operators {
|
||||
if oper.Auto && oper.Pass == nil && oper.Fingerprint != "" && oper.Fingerprint == session.certfp {
|
||||
if oper.Auto && oper.Pass == nil && oper.Certfp != "" && oper.Certfp == session.certfp {
|
||||
rb := NewResponseBuffer(session)
|
||||
applyOper(client, oper, rb)
|
||||
rb.Send(true)
|
||||
@@ -1604,6 +1772,12 @@ func (client *Client) attemptAutoOper(session *Session) {
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) checkLoginThrottle() (throttled bool, remainingTime time.Duration) {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
return client.loginThrottle.Touch()
|
||||
}
|
||||
|
||||
func (client *Client) historyStatus(config *Config) (status HistoryStatus, target string) {
|
||||
if !config.History.Enabled {
|
||||
return HistoryDisabled, ""
|
||||
@@ -1624,12 +1798,28 @@ func (client *Client) historyStatus(config *Config) (status HistoryStatus, targe
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) handleRegisterTimeout() {
|
||||
client.Quit(fmt.Sprintf("Registration timeout: %v", RegisterTimeout), nil)
|
||||
client.destroy(nil)
|
||||
}
|
||||
|
||||
func (client *Client) copyLastSeen() (result map[string]time.Time) {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
result = make(map[string]time.Time, len(client.lastSeen))
|
||||
for id, lastSeen := range client.lastSeen {
|
||||
result[id] = lastSeen
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// these are bit flags indicating what part of the client status is "dirty"
|
||||
// and needs to be read from memory and written to the db
|
||||
const (
|
||||
IncludeChannels uint = 1 << iota
|
||||
IncludeLastSeen
|
||||
IncludeUserModes
|
||||
IncludeRealname
|
||||
)
|
||||
|
||||
func (client *Client) markDirty(dirtyBits uint) {
|
||||
@@ -1651,7 +1841,7 @@ func (client *Client) wakeWriter() {
|
||||
|
||||
func (client *Client) writeLoop() {
|
||||
for {
|
||||
client.performWrite()
|
||||
client.performWrite(0)
|
||||
client.writerSemaphore.Release()
|
||||
|
||||
client.stateMutex.RLock()
|
||||
@@ -1664,12 +1854,11 @@ func (client *Client) writeLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) performWrite() {
|
||||
func (client *Client) performWrite(additionalDirtyBits uint) {
|
||||
client.stateMutex.Lock()
|
||||
dirtyBits := client.dirtyBits
|
||||
dirtyBits := client.dirtyBits | additionalDirtyBits
|
||||
client.dirtyBits = 0
|
||||
account := client.account
|
||||
lastSeen := client.lastSeen
|
||||
client.stateMutex.Unlock()
|
||||
|
||||
if account == "" {
|
||||
@@ -1686,7 +1875,7 @@ func (client *Client) performWrite() {
|
||||
client.server.accounts.saveChannels(account, channelNames)
|
||||
}
|
||||
if (dirtyBits & IncludeLastSeen) != 0 {
|
||||
client.server.accounts.saveLastSeen(account, lastSeen)
|
||||
client.server.accounts.saveLastSeen(account, client.copyLastSeen())
|
||||
}
|
||||
if (dirtyBits & IncludeUserModes) != 0 {
|
||||
uModes := make(modes.Modes, 0, len(modes.SupportedUserModes))
|
||||
@@ -1702,4 +1891,25 @@ func (client *Client) performWrite() {
|
||||
}
|
||||
client.server.accounts.saveModes(account, uModes)
|
||||
}
|
||||
if (dirtyBits & IncludeRealname) != 0 {
|
||||
client.server.accounts.saveRealname(account, client.realname)
|
||||
}
|
||||
}
|
||||
|
||||
// Blocking store; see Channel.Store and Socket.BlockingWrite
|
||||
func (client *Client) Store(dirtyBits uint) (err error) {
|
||||
defer func() {
|
||||
client.stateMutex.Lock()
|
||||
isDirty := client.dirtyBits != 0
|
||||
client.stateMutex.Unlock()
|
||||
|
||||
if isDirty {
|
||||
client.wakeWriter()
|
||||
}
|
||||
}()
|
||||
|
||||
client.writerSemaphore.Acquire()
|
||||
defer client.writerSemaphore.Release()
|
||||
client.performWrite(dirtyBits)
|
||||
return nil
|
||||
}
|
||||
|
||||
+13
-39
@@ -28,14 +28,6 @@ func (clients *ClientManager) Initialize() {
|
||||
clients.bySkeleton = make(map[string]*Client)
|
||||
}
|
||||
|
||||
// Count returns how many clients are in the manager.
|
||||
func (clients *ClientManager) Count() int {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
count := len(clients.byNick)
|
||||
return count
|
||||
}
|
||||
|
||||
// Get retrieves a client from the manager, if they exist.
|
||||
func (clients *ClientManager) Get(nick string) *Client {
|
||||
casefoldedName, err := CasefoldName(nick)
|
||||
@@ -48,9 +40,8 @@ func (clients *ClientManager) Get(nick string) *Client {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (clients *ClientManager) removeInternal(client *Client) (err error) {
|
||||
func (clients *ClientManager) removeInternal(client *Client, oldcfnick, oldskeleton string) (err error) {
|
||||
// requires holding the writable Lock()
|
||||
oldcfnick, oldskeleton := client.uniqueIdentifiers()
|
||||
if oldcfnick == "*" || oldcfnick == "" {
|
||||
return errNickMissing
|
||||
}
|
||||
@@ -88,7 +79,8 @@ func (clients *ClientManager) Remove(client *Client) error {
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
return clients.removeInternal(client)
|
||||
oldcfnick, oldskeleton := client.uniqueIdentifiers()
|
||||
return clients.removeInternal(client, oldcfnick, oldskeleton)
|
||||
}
|
||||
|
||||
// Handles a RESUME by attaching a session to a designated client. It is the
|
||||
@@ -173,7 +165,6 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
|
||||
config := client.server.Config()
|
||||
if config.Server.Relaying.Enabled {
|
||||
for _, char := range config.Server.Relaying.Separators {
|
||||
if strings.ContainsRune(newCfNick, char) {
|
||||
@@ -182,7 +173,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
}
|
||||
}
|
||||
|
||||
if restrictedCasefoldedNicks[newCfNick] || restrictedSkeletons[newSkeleton] {
|
||||
if restrictedCasefoldedNicks.Has(newCfNick) || restrictedSkeletons.Has(newSkeleton) {
|
||||
return "", errNicknameInvalid, false
|
||||
}
|
||||
|
||||
@@ -234,18 +225,13 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
client.server.stats.AddRegistered(invisible, operator)
|
||||
}
|
||||
session.autoreplayMissedSince = lastSeen
|
||||
// XXX SetNames only changes names if they are unset, so the realname change only
|
||||
// takes effect on first attach to an always-on client (good), but the user/ident
|
||||
// change is always a no-op (bad). we could make user/ident act the same way as
|
||||
// realname, but then we'd have to send CHGHOST and i don't want to deal with that
|
||||
// for performance reasons
|
||||
currentClient.SetNames("user", realname, true)
|
||||
// TODO: transition mechanism for #1065, clean this up eventually:
|
||||
if currentClient.Realname() == "" {
|
||||
currentClient.SetRealname(realname)
|
||||
}
|
||||
// successful reattach!
|
||||
return newNick, nil, back
|
||||
} else if currentClient == client && currentClient.Nick() == newNick {
|
||||
// see #1019: normally no-op nick changes are caught earlier, by performNickChange,
|
||||
// but they are not detected there when force-guest-format is enabled (because
|
||||
// the proposed nickname is e.g. alice and the current nickname is Guest-alice)
|
||||
return "", errNoop, false
|
||||
}
|
||||
// analogous checks for skeletons
|
||||
@@ -254,10 +240,13 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
|
||||
return "", errNicknameInUse, false
|
||||
}
|
||||
|
||||
clients.removeInternal(client)
|
||||
formercfnick, formerskeleton := client.uniqueIdentifiers()
|
||||
if changeSuccess := client.SetNick(newNick, newCfNick, newSkeleton); !changeSuccess {
|
||||
return "", errClientDestroyed, false
|
||||
}
|
||||
clients.removeInternal(client, formercfnick, formerskeleton)
|
||||
clients.byNick[newCfNick] = client
|
||||
clients.bySkeleton[newSkeleton] = client
|
||||
client.updateNick(newNick, newCfNick, newSkeleton)
|
||||
return newNick, nil, false
|
||||
}
|
||||
|
||||
@@ -273,21 +262,6 @@ func (clients *ClientManager) AllClients() (result []*Client) {
|
||||
return
|
||||
}
|
||||
|
||||
// AllWithCaps returns all clients with the given capabilities.
|
||||
func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (sessions []*Session) {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
for _, client := range clients.byNick {
|
||||
for _, session := range client.Sessions() {
|
||||
if session.capabilities.HasAll(capabs...) {
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AllWithCapsNotify returns all clients with the given capabilities, and that support cap-notify.
|
||||
func (clients *ClientManager) AllWithCapsNotify(capabs ...caps.Capability) (sessions []*Session) {
|
||||
capabs = append(capabs, caps.CapNotify)
|
||||
|
||||
+33
-1
@@ -5,11 +5,13 @@ package irc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
func TestGenerateBatchID(t *testing.T) {
|
||||
var session Session
|
||||
s := make(StringSet)
|
||||
s := make(utils.StringSet)
|
||||
|
||||
count := 100000
|
||||
for i := 0; i < count; i++ {
|
||||
@@ -56,3 +58,33 @@ func TestUserMasks(t *testing.T) {
|
||||
t.Error("failure to match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoFields(t *testing.T) {
|
||||
var w whoxFields
|
||||
|
||||
if w.Has('a') {
|
||||
t.Error("zero value of whoxFields must be empty")
|
||||
}
|
||||
w = w.Add('a')
|
||||
if !w.Has('a') {
|
||||
t.Error("failed to set and get")
|
||||
}
|
||||
if w.Has('A') {
|
||||
t.Error("false positive")
|
||||
}
|
||||
if w.Has('o') {
|
||||
t.Error("false positive")
|
||||
}
|
||||
w = w.Add('🐬')
|
||||
if w.Has('🐬') {
|
||||
t.Error("should not be able to set invalid who field")
|
||||
}
|
||||
w = w.Add('o')
|
||||
if !w.Has('o') {
|
||||
t.Error("failed to set and get")
|
||||
}
|
||||
w = w.Add('z')
|
||||
if !w.Has('z') {
|
||||
t.Error("failed to set and get")
|
||||
}
|
||||
}
|
||||
|
||||
+30
-29
@@ -12,13 +12,12 @@ import (
|
||||
|
||||
// Command represents a command accepted from a client.
|
||||
type Command struct {
|
||||
handler func(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool
|
||||
oper bool
|
||||
usablePreReg bool
|
||||
leaveClientIdle bool // if true, leaves the client active time alone
|
||||
allowedInBatch bool // allowed in client-to-server batches
|
||||
minParams int
|
||||
capabs []string
|
||||
handler func(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool
|
||||
oper bool
|
||||
usablePreReg bool
|
||||
allowedInBatch bool // allowed in client-to-server batches
|
||||
minParams int
|
||||
capabs []string
|
||||
}
|
||||
|
||||
// Run runs this command with the given client/message.
|
||||
@@ -59,15 +58,8 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
|
||||
exiting = server.tryRegister(client, session)
|
||||
}
|
||||
|
||||
// most servers do this only for PING/PONG, but we'll do it for any command:
|
||||
if client.registered {
|
||||
// touch even if `exiting`, so we record the time of a QUIT accurately
|
||||
session.idletimer.Touch()
|
||||
}
|
||||
|
||||
// TODO: eliminate idletimer entirely in favor of this measurement
|
||||
if client.registered {
|
||||
client.Touch(!cmd.leaveClientIdle, session)
|
||||
client.Touch(session)
|
||||
}
|
||||
|
||||
return exiting
|
||||
@@ -79,6 +71,11 @@ var unknownCommand = Command{
|
||||
usablePreReg: true,
|
||||
}
|
||||
|
||||
var invalidUtf8Command = Command{
|
||||
handler: invalidUtf8Handler,
|
||||
usablePreReg: true,
|
||||
}
|
||||
|
||||
// Commands holds all commands executable by a client connected to us.
|
||||
var Commands map[string]Command
|
||||
|
||||
@@ -120,6 +117,10 @@ func init() {
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
},
|
||||
"DEFCON": {
|
||||
handler: defconHandler,
|
||||
capabs: []string{"defcon"},
|
||||
},
|
||||
"DEOPER": {
|
||||
handler: deoperHandler,
|
||||
minParams: 0,
|
||||
@@ -130,6 +131,10 @@ func init() {
|
||||
minParams: 1,
|
||||
oper: true,
|
||||
},
|
||||
"EXTJWT": {
|
||||
handler: extjwtHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"HELP": {
|
||||
handler: helpHandler,
|
||||
minParams: 0,
|
||||
@@ -150,9 +155,8 @@ func init() {
|
||||
minParams: 2,
|
||||
},
|
||||
"ISON": {
|
||||
handler: isonHandler,
|
||||
minParams: 1,
|
||||
leaveClientIdle: true,
|
||||
handler: isonHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"JOIN": {
|
||||
handler: joinHandler,
|
||||
@@ -234,16 +238,14 @@ func init() {
|
||||
minParams: 1,
|
||||
},
|
||||
"PING": {
|
||||
handler: pingHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
leaveClientIdle: true,
|
||||
handler: pingHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"PONG": {
|
||||
handler: pongHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
leaveClientIdle: true,
|
||||
handler: pongHandler,
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"PRIVMSG": {
|
||||
handler: messageHandler,
|
||||
@@ -344,9 +346,8 @@ func init() {
|
||||
minParams: 4,
|
||||
},
|
||||
"WHO": {
|
||||
handler: whoHandler,
|
||||
minParams: 1,
|
||||
leaveClientIdle: true,
|
||||
handler: whoHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"WHOIS": {
|
||||
handler: whoisHandler,
|
||||
|
||||
+129
-36
@@ -6,6 +6,7 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -21,20 +22,22 @@ import (
|
||||
"time"
|
||||
|
||||
"code.cloudfoundry.org/bytefmt"
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/cloaks"
|
||||
"github.com/oragono/oragono/irc/connection_limits"
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/email"
|
||||
"github.com/oragono/oragono/irc/isupport"
|
||||
"github.com/oragono/oragono/irc/jwt"
|
||||
"github.com/oragono/oragono/irc/languages"
|
||||
"github.com/oragono/oragono/irc/ldap"
|
||||
"github.com/oragono/oragono/irc/logger"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/mysql"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// here's how this works: exported (capitalized) members of the config structs
|
||||
@@ -257,7 +260,6 @@ type AccountConfig struct {
|
||||
} `yaml:"require-sasl"`
|
||||
DefaultUserModes *string `yaml:"default-user-modes"`
|
||||
defaultUserModes modes.Modes
|
||||
LDAP ldap.ServerConfig
|
||||
LoginThrottling ThrottleConfig `yaml:"login-throttling"`
|
||||
SkipServerPassword bool `yaml:"skip-server-password"`
|
||||
LoginViaPassCommand bool `yaml:"login-via-pass-command"`
|
||||
@@ -265,8 +267,7 @@ type AccountConfig struct {
|
||||
Enabled bool
|
||||
AdditionalNickLimit int `yaml:"additional-nick-limit"`
|
||||
Method NickEnforcementMethod
|
||||
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
|
||||
RenameTimeout time.Duration `yaml:"rename-timeout"`
|
||||
AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"`
|
||||
// RenamePrefix is the legacy field, GuestFormat is the new version
|
||||
RenamePrefix string `yaml:"rename-prefix"`
|
||||
GuestFormat string `yaml:"guest-nickname-format"`
|
||||
@@ -313,7 +314,6 @@ type VHostConfig struct {
|
||||
Channel string
|
||||
Cooldown custime.Duration
|
||||
} `yaml:"user-requests"`
|
||||
OfferList []string `yaml:"offer-list"`
|
||||
}
|
||||
|
||||
type NickEnforcementMethod int
|
||||
@@ -409,7 +409,8 @@ type OperConfig struct {
|
||||
Vhost string
|
||||
WhoisLine string `yaml:"whois-line"`
|
||||
Password string
|
||||
Fingerprint string
|
||||
Fingerprint *string // legacy name for certfp, #1050
|
||||
Certfp string
|
||||
Auto bool
|
||||
Modes string
|
||||
}
|
||||
@@ -497,6 +498,7 @@ type Config struct {
|
||||
lookupHostnames bool
|
||||
ForwardConfirmHostnames bool `yaml:"forward-confirm-hostnames"`
|
||||
CheckIdent bool `yaml:"check-ident"`
|
||||
SuppressIdent bool `yaml:"suppress-ident"`
|
||||
MOTD string
|
||||
motdLines []string
|
||||
MOTDFormatting bool `yaml:"motd-formatting"`
|
||||
@@ -524,6 +526,7 @@ type Config struct {
|
||||
supportedCaps *caps.Set
|
||||
capValues caps.Values
|
||||
Casemapping Casemapping
|
||||
EnforceUtf8 bool `yaml:"enforce-utf8"`
|
||||
OutputPath string `yaml:"output-path"`
|
||||
}
|
||||
|
||||
@@ -536,6 +539,11 @@ type Config struct {
|
||||
addSuffix bool
|
||||
}
|
||||
|
||||
Extjwt struct {
|
||||
Default jwt.JwtServiceConfig `yaml:",inline"`
|
||||
Services map[string]jwt.JwtServiceConfig `yaml:"services"`
|
||||
}
|
||||
|
||||
Languages struct {
|
||||
Enabled bool
|
||||
Path string
|
||||
@@ -608,6 +616,11 @@ type Config struct {
|
||||
AllowIndividualDelete bool `yaml:"allow-individual-delete"`
|
||||
EnableAccountIndexing bool `yaml:"enable-account-indexing"`
|
||||
}
|
||||
TagmsgStorage struct {
|
||||
Default bool
|
||||
Whitelist []string
|
||||
Blacklist []string
|
||||
} `yaml:"tagmsg-storage"`
|
||||
}
|
||||
|
||||
Filename string
|
||||
@@ -616,8 +629,8 @@ type Config struct {
|
||||
// OperClass defines an assembled operator class.
|
||||
type OperClass struct {
|
||||
Title string
|
||||
WhoisLine string `yaml:"whois-line"`
|
||||
Capabilities StringSet // map to make lookups much easier
|
||||
WhoisLine string `yaml:"whois-line"`
|
||||
Capabilities utils.StringSet // map to make lookups much easier
|
||||
}
|
||||
|
||||
// OperatorClasses returns a map of assembled operator classes from the given config.
|
||||
@@ -632,7 +645,7 @@ func (conf *Config) OperatorClasses() (map[string]*OperClass, error) {
|
||||
lenOfLastOcs := -1
|
||||
for {
|
||||
if lenOfLastOcs == len(ocs) {
|
||||
return nil, ErrOperClassDependencies
|
||||
return nil, errors.New("OperClasses contains a looping dependency, or a class extends from a class that doesn't exist")
|
||||
}
|
||||
lenOfLastOcs = len(ocs)
|
||||
|
||||
@@ -655,7 +668,7 @@ func (conf *Config) OperatorClasses() (map[string]*OperClass, error) {
|
||||
|
||||
// create new operclass
|
||||
var oc OperClass
|
||||
oc.Capabilities = make(StringSet)
|
||||
oc.Capabilities = make(utils.StringSet)
|
||||
|
||||
// get inhereted info from other operclasses
|
||||
if len(info.Extends) > 0 {
|
||||
@@ -696,14 +709,14 @@ func (conf *Config) OperatorClasses() (map[string]*OperClass, error) {
|
||||
|
||||
// Oper represents a single assembled operator's config.
|
||||
type Oper struct {
|
||||
Name string
|
||||
Class *OperClass
|
||||
WhoisLine string
|
||||
Vhost string
|
||||
Pass []byte
|
||||
Fingerprint string
|
||||
Auto bool
|
||||
Modes []modes.ModeChange
|
||||
Name string
|
||||
Class *OperClass
|
||||
WhoisLine string
|
||||
Vhost string
|
||||
Pass []byte
|
||||
Certfp string
|
||||
Auto bool
|
||||
Modes []modes.ModeChange
|
||||
}
|
||||
|
||||
// Operators returns a map of operator configs from the given OperClass and config.
|
||||
@@ -725,15 +738,19 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
|
||||
return nil, fmt.Errorf("Oper %s has an invalid password hash: %s", oper.Name, err.Error())
|
||||
}
|
||||
}
|
||||
if opConf.Fingerprint != "" {
|
||||
oper.Fingerprint, err = utils.NormalizeCertfp(opConf.Fingerprint)
|
||||
certfp := opConf.Certfp
|
||||
if certfp == "" && opConf.Fingerprint != nil {
|
||||
certfp = *opConf.Fingerprint
|
||||
}
|
||||
if certfp != "" {
|
||||
oper.Certfp, err = utils.NormalizeCertfp(certfp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Oper %s has an invalid fingerprint: %s", oper.Name, err.Error())
|
||||
}
|
||||
}
|
||||
oper.Auto = opConf.Auto
|
||||
|
||||
if oper.Pass == nil && oper.Fingerprint == "" {
|
||||
if oper.Pass == nil && oper.Certfp == "" {
|
||||
return nil, fmt.Errorf("Oper %s has neither a password nor a fingerprint", name)
|
||||
}
|
||||
|
||||
@@ -810,6 +827,29 @@ func (conf *Config) prepareListeners() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (config *Config) processExtjwt() (err error) {
|
||||
// first process the default service, which may be disabled
|
||||
err = config.Extjwt.Default.Postprocess()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// now process the named services. it is an error if any is disabled
|
||||
// also, normalize the service names to lowercase
|
||||
services := make(map[string]jwt.JwtServiceConfig, len(config.Extjwt.Services))
|
||||
for service, sConf := range config.Extjwt.Services {
|
||||
err := sConf.Postprocess()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !sConf.Enabled() {
|
||||
return fmt.Errorf("no keys enabled for extjwt service %s", service)
|
||||
}
|
||||
services[strings.ToLower(service)] = sConf
|
||||
}
|
||||
config.Extjwt.Services = services
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadRawConfig loads the config without doing any consistency checks or postprocessing
|
||||
func LoadRawConfig(filename string) (config *Config, err error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
@@ -834,24 +874,24 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
config.Filename = filename
|
||||
|
||||
if config.Network.Name == "" {
|
||||
return nil, ErrNetworkNameMissing
|
||||
return nil, errors.New("Network name missing")
|
||||
}
|
||||
if config.Server.Name == "" {
|
||||
return nil, ErrServerNameMissing
|
||||
return nil, errors.New("Server name missing")
|
||||
}
|
||||
if !utils.IsServerName(config.Server.Name) {
|
||||
return nil, ErrServerNameNotHostname
|
||||
return nil, errors.New("Server name must match the format of a hostname")
|
||||
}
|
||||
config.Server.nameCasefolded = strings.ToLower(config.Server.Name)
|
||||
if config.Datastore.Path == "" {
|
||||
return nil, ErrDatastorePathMissing
|
||||
return nil, errors.New("Datastore path missing")
|
||||
}
|
||||
//dan: automagically fix identlen until a few releases in the future (from now, 0.12.0), being a newly-introduced limit
|
||||
if config.Limits.IdentLen < 1 {
|
||||
config.Limits.IdentLen = 20
|
||||
}
|
||||
if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 {
|
||||
return nil, ErrLimitsAreInsane
|
||||
return nil, errors.New("One or more limits values are too low")
|
||||
}
|
||||
if config.Limits.RegistrationMessages == 0 {
|
||||
config.Limits.RegistrationMessages = 1024
|
||||
@@ -862,6 +902,10 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if config.Server.CheckIdent && config.Server.SuppressIdent {
|
||||
return nil, errors.New("Can't configure both check-ident and suppress-ident")
|
||||
}
|
||||
|
||||
config.Server.supportedCaps = caps.NewCompleteSet()
|
||||
config.Server.capValues = make(caps.Values)
|
||||
|
||||
@@ -964,7 +1008,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
}
|
||||
if methods["file"] && logConfig.Filename == "" {
|
||||
return nil, ErrLoggerFilenameMissing
|
||||
return nil, errors.New("Logging configuration specifies 'file' method but 'filename' is empty")
|
||||
}
|
||||
logConfig.MethodFile = methods["file"]
|
||||
logConfig.MethodStdout = methods["stdout"]
|
||||
@@ -983,7 +1027,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
continue
|
||||
}
|
||||
if typeStr == "-" {
|
||||
return nil, ErrLoggerExcludeEmpty
|
||||
return nil, errors.New("Encountered logging type '-' with no type to exclude")
|
||||
}
|
||||
if typeStr[0] == '-' {
|
||||
typeStr = typeStr[1:]
|
||||
@@ -993,7 +1037,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
}
|
||||
if len(logConfig.Types) < 1 {
|
||||
return nil, ErrLoggerHasNoTypes
|
||||
return nil, errors.New("Logger has no types to log")
|
||||
}
|
||||
|
||||
newLogConfigs = append(newLogConfigs, logConfig)
|
||||
@@ -1050,12 +1094,6 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
|
||||
}
|
||||
|
||||
for _, vhost := range config.Accounts.VHosts.OfferList {
|
||||
if !config.Accounts.VHosts.ValidRegexp.MatchString(vhost) {
|
||||
return nil, fmt.Errorf("invalid offered vhost: %s", vhost)
|
||||
}
|
||||
}
|
||||
|
||||
config.Server.capValues[caps.SASL] = "PLAIN,EXTERNAL"
|
||||
if !config.Accounts.AuthenticationEnabled {
|
||||
config.Server.supportedCaps.Disable(caps.SASL)
|
||||
@@ -1134,11 +1172,16 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
|
||||
if !config.History.Enabled || !config.History.Persistent.Enabled {
|
||||
config.History.Persistent.Enabled = false
|
||||
config.History.Persistent.UnregisteredChannels = false
|
||||
config.History.Persistent.RegisteredChannels = PersistentDisabled
|
||||
config.History.Persistent.DirectMessages = PersistentDisabled
|
||||
}
|
||||
|
||||
if config.History.Persistent.Enabled && !config.Datastore.MySQL.Enabled {
|
||||
return nil, fmt.Errorf("You must configure a MySQL server in order to enable persistent history")
|
||||
}
|
||||
|
||||
if config.History.ZNCMax == 0 {
|
||||
config.History.ZNCMax = config.History.ChathistoryMax
|
||||
}
|
||||
@@ -1156,6 +1199,11 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
err = config.processExtjwt()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// now that all postprocessing is complete, regenerate ISUPPORT:
|
||||
err = config.generateISupport()
|
||||
if err != nil {
|
||||
@@ -1193,6 +1241,9 @@ func (config *Config) generateISupport() (err error) {
|
||||
isupport.Add("CHANTYPES", chanTypes)
|
||||
isupport.Add("ELIST", "U")
|
||||
isupport.Add("EXCEPTS", "")
|
||||
if config.Extjwt.Default.Enabled() || len(config.Extjwt.Services) != 0 {
|
||||
isupport.Add("EXTJWT", "1")
|
||||
}
|
||||
isupport.Add("INVEX", "")
|
||||
isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen))
|
||||
isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes)))
|
||||
@@ -1212,6 +1263,7 @@ func (config *Config) generateISupport() (err error) {
|
||||
if config.Server.Casemapping == CasemappingPRECIS {
|
||||
isupport.Add("UTF8MAPPING", precisUTF8MappingToken)
|
||||
}
|
||||
isupport.Add("WHOX", "")
|
||||
|
||||
err = isupport.RegenerateCachedReply()
|
||||
return
|
||||
@@ -1255,6 +1307,16 @@ func (config *Config) Diff(oldConfig *Config) (addedCaps, removedCaps *caps.Set)
|
||||
return
|
||||
}
|
||||
|
||||
// determine whether we need to resize / create / destroy
|
||||
// the in-memory history buffers:
|
||||
func (config *Config) historyChangedFrom(oldConfig *Config) bool {
|
||||
return config.History.Enabled != oldConfig.History.Enabled ||
|
||||
config.History.ChannelLength != oldConfig.History.ChannelLength ||
|
||||
config.History.ClientLength != oldConfig.History.ClientLength ||
|
||||
config.History.AutoresizeWindow != oldConfig.History.AutoresizeWindow ||
|
||||
config.History.Persistent != oldConfig.History.Persistent
|
||||
}
|
||||
|
||||
func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard, folded *regexp.Regexp, err error) {
|
||||
if strings.Count(guestFormat, "?") != 0 || strings.Count(guestFormat, "*") != 1 {
|
||||
err = errors.New("guest format must contain 1 '*' and no '?'s")
|
||||
@@ -1280,3 +1342,34 @@ func compileGuestRegexp(guestFormat string, casemapping Casemapping) (standard,
|
||||
folded, err = utils.CompileGlob(fmt.Sprintf("%s*%s", initialFolded, finalFolded), false)
|
||||
return
|
||||
}
|
||||
|
||||
func (config *Config) loadMOTD() error {
|
||||
if config.Server.MOTD != "" {
|
||||
file, err := os.Open(config.Server.MOTD)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
contents, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := bytes.Split(contents, []byte{'\n'})
|
||||
for i, line := range lines {
|
||||
lineToSend := string(bytes.TrimRight(line, "\r\n"))
|
||||
if len(lineToSend) == 0 && i == len(lines)-1 {
|
||||
// if the last line of the MOTD was properly terminated with \n,
|
||||
// there's no need to send a blank line to clients
|
||||
continue
|
||||
}
|
||||
if config.Server.MOTDFormatting {
|
||||
lineToSend = ircfmt.Unescape(lineToSend)
|
||||
}
|
||||
// "- " is the required prefix for MOTD
|
||||
lineToSend = fmt.Sprintf("- %s", lineToSend)
|
||||
config.Server.motdLines = append(config.Server.motdLines, lineToSend)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+14
-23
@@ -8,6 +8,7 @@ package irc
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
@@ -57,7 +58,6 @@ var (
|
||||
errBanned = errors.New("IP or nickmask banned")
|
||||
errInvalidParams = utils.ErrInvalidParams
|
||||
errNoVhost = errors.New(`You do not have an approved vhost`)
|
||||
errVhostsForbidden = errors.New(`An administrator has denied you the ability to use vhosts`)
|
||||
errLimitExceeded = errors.New("Limit exceeded")
|
||||
errNoop = errors.New("Action was a no-op")
|
||||
errCASFailed = errors.New("Compare-and-swap update of database value failed")
|
||||
@@ -65,13 +65,12 @@ var (
|
||||
errCredsExternallyManaged = errors.New("Credentials are externally managed and cannot be changed here")
|
||||
errInvalidMultilineBatch = errors.New("Invalid multiline batch")
|
||||
errTimedOut = errors.New("Operation timed out")
|
||||
)
|
||||
|
||||
// Socket Errors
|
||||
var (
|
||||
errNoPeerCerts = errors.New("Client did not provide a certificate")
|
||||
errNotTLS = errors.New("Not a TLS connection")
|
||||
errReadQ = errors.New("ReadQ Exceeded")
|
||||
errInvalidUtf8 = errors.New("Message rejected for invalid utf8")
|
||||
errClientDestroyed = errors.New("Client was already destroyed")
|
||||
errTooManyChannels = errors.New("You have joined too many channels")
|
||||
errWrongChannelKey = errors.New("Cannot join password-protected channel without the password")
|
||||
errInviteOnly = errors.New("Cannot join invite-only channel without an invite")
|
||||
errRegisteredOnly = errors.New("Cannot join registered-only channel without an account")
|
||||
)
|
||||
|
||||
// String Errors
|
||||
@@ -89,18 +88,10 @@ func (ck *CertKeyError) Error() string {
|
||||
return fmt.Sprintf("Invalid TLS cert/key pair: %v", ck.Err)
|
||||
}
|
||||
|
||||
// Config Errors
|
||||
var (
|
||||
ErrDatastorePathMissing = errors.New("Datastore path missing")
|
||||
ErrLimitsAreInsane = errors.New("Limits aren't setup properly, check them and make them sane")
|
||||
ErrLineLengthsTooSmall = errors.New("Line lengths must be 512 or greater (check the linelen section under server->limits)")
|
||||
ErrLoggerExcludeEmpty = errors.New("Encountered logging type '-' with no type to exclude")
|
||||
ErrLoggerFilenameMissing = errors.New("Logging configuration specifies 'file' method but 'filename' is empty")
|
||||
ErrLoggerHasNoTypes = errors.New("Logger has no types to log")
|
||||
ErrNetworkNameMissing = errors.New("Network name missing")
|
||||
ErrNoFingerprintOrPassword = errors.New("Fingerprint or password needs to be specified")
|
||||
ErrNoListenersDefined = errors.New("Server listening addresses missing")
|
||||
ErrOperClassDependencies = errors.New("OperClasses contains a looping dependency, or a class extends from a class that doesn't exist")
|
||||
ErrServerNameMissing = errors.New("Server name missing")
|
||||
ErrServerNameNotHostname = errors.New("Server name must match the format of a hostname")
|
||||
)
|
||||
type ThrottleError struct {
|
||||
time.Duration
|
||||
}
|
||||
|
||||
func (te *ThrottleError) Error() string {
|
||||
return fmt.Sprintf(`Please wait at least %v and try again`, te.Duration)
|
||||
}
|
||||
|
||||
+20
-12
@@ -26,31 +26,39 @@ const (
|
||||
)
|
||||
|
||||
type webircConfig struct {
|
||||
PasswordString string `yaml:"password"`
|
||||
Password []byte `yaml:"password-bytes"`
|
||||
Fingerprint string
|
||||
PasswordString string `yaml:"password"`
|
||||
Password []byte `yaml:"password-bytes"`
|
||||
Fingerprint *string // legacy name for certfp, #1050
|
||||
Certfp string
|
||||
Hosts []string
|
||||
allowedNets []net.IPNet
|
||||
}
|
||||
|
||||
// Populate fills out our password or fingerprint.
|
||||
func (wc *webircConfig) Populate() (err error) {
|
||||
if wc.Fingerprint == "" && wc.PasswordString == "" {
|
||||
err = ErrNoFingerprintOrPassword
|
||||
}
|
||||
|
||||
if err == nil && wc.PasswordString != "" {
|
||||
if wc.PasswordString != "" {
|
||||
wc.Password, err = decodeLegacyPasswordHash(wc.PasswordString)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && wc.Fingerprint != "" {
|
||||
wc.Fingerprint, err = utils.NormalizeCertfp(wc.Fingerprint)
|
||||
certfp := wc.Certfp
|
||||
if certfp == "" && wc.Fingerprint != nil {
|
||||
certfp = *wc.Fingerprint
|
||||
}
|
||||
if certfp != "" {
|
||||
wc.Certfp, err = utils.NormalizeCertfp(certfp)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
wc.allowedNets, err = utils.ParseNetList(wc.Hosts)
|
||||
if wc.Certfp == "" && wc.PasswordString == "" {
|
||||
return errors.New("webirc block has no certfp or password specified")
|
||||
}
|
||||
|
||||
wc.allowedNets, err = utils.ParseNetList(wc.Hosts)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+60
-33
@@ -37,6 +37,14 @@ func (server *Server) Languages() (lm *languages.Manager) {
|
||||
return server.Config().languageManager
|
||||
}
|
||||
|
||||
func (server *Server) Defcon() uint32 {
|
||||
return atomic.LoadUint32(&server.defcon)
|
||||
}
|
||||
|
||||
func (server *Server) SetDefcon(defcon uint32) {
|
||||
atomic.StoreUint32(&server.defcon, defcon)
|
||||
}
|
||||
|
||||
func (client *Client) Sessions() (sessions []*Session) {
|
||||
client.stateMutex.RLock()
|
||||
sessions = client.sessions
|
||||
@@ -62,6 +70,7 @@ type SessionData struct {
|
||||
ip net.IP
|
||||
hostname string
|
||||
certfp string
|
||||
deviceID string
|
||||
}
|
||||
|
||||
func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
|
||||
@@ -79,6 +88,7 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat
|
||||
ctime: session.ctime,
|
||||
hostname: session.rawHostname,
|
||||
certfp: session.certfp,
|
||||
deviceID: session.deviceID,
|
||||
}
|
||||
if session.proxiedIP != nil {
|
||||
data[i].ip = session.proxiedIP
|
||||
@@ -102,8 +112,9 @@ func (client *Client) AddSession(session *Session) (success bool, numSessions in
|
||||
newSessions := make([]*Session, len(client.sessions)+1)
|
||||
copy(newSessions, client.sessions)
|
||||
newSessions[len(newSessions)-1] = session
|
||||
if client.accountSettings.AutoreplayMissed {
|
||||
lastSeen = client.lastSeen
|
||||
if client.accountSettings.AutoreplayMissed || session.deviceID != "" {
|
||||
lastSeen = client.lastSeen[session.deviceID]
|
||||
client.setLastSeen(time.Now().UTC(), session.deviceID)
|
||||
}
|
||||
client.sessions = newSessions
|
||||
if client.autoAway {
|
||||
@@ -174,9 +185,9 @@ func (client *Client) Hostname() string {
|
||||
return client.hostname
|
||||
}
|
||||
|
||||
func (client *Client) Away() (result bool) {
|
||||
func (client *Client) Away() (result bool, message string) {
|
||||
client.stateMutex.Lock()
|
||||
result = client.away
|
||||
result, message = client.away, client.awayMessage
|
||||
client.stateMutex.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -190,15 +201,9 @@ func (client *Client) SetAway(away bool, awayMessage string) (changed bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) SetExitedSnomaskSent() {
|
||||
client.stateMutex.Lock()
|
||||
client.exitedSnomaskSent = true
|
||||
client.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
func (client *Client) AlwaysOn() (alwaysOn bool) {
|
||||
client.stateMutex.Lock()
|
||||
alwaysOn = client.alwaysOn
|
||||
alwaysOn = client.registered && client.alwaysOn
|
||||
client.stateMutex.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -230,20 +235,15 @@ func (client *Client) Oper() *Oper {
|
||||
return client.oper
|
||||
}
|
||||
|
||||
func (client *Client) Registered() bool {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.registered
|
||||
}
|
||||
|
||||
func (client *Client) SetRegistered() {
|
||||
func (client *Client) Registered() (result bool) {
|
||||
// `registered` is only written from the client's own goroutine, but may be
|
||||
// read from other goroutines; therefore, the client's own goroutine may read
|
||||
// the value without synchronization, but must write it with synchronization,
|
||||
// and other goroutines must read it with synchronization
|
||||
client.stateMutex.Lock()
|
||||
client.registered = true
|
||||
client.stateMutex.Unlock()
|
||||
client.stateMutex.RLock()
|
||||
result = client.registered
|
||||
client.stateMutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) RawHostname() (result string) {
|
||||
@@ -285,11 +285,8 @@ func (client *Client) Login(account ClientAccount) {
|
||||
client.account = account.NameCasefolded
|
||||
client.accountName = account.Name
|
||||
client.accountSettings = account.Settings
|
||||
// check `registered` to avoid incorrectly marking a temporary (pre-reattach),
|
||||
// SASL'ing client as always-on
|
||||
if client.registered {
|
||||
client.alwaysOn = alwaysOn
|
||||
}
|
||||
// mark always-on here: it will not be respected until the client is registered
|
||||
client.alwaysOn = alwaysOn
|
||||
client.accountRegDate = account.RegisteredAt
|
||||
return
|
||||
}
|
||||
@@ -324,17 +321,26 @@ func (client *Client) AccountSettings() (result AccountSettings) {
|
||||
|
||||
func (client *Client) SetAccountSettings(settings AccountSettings) {
|
||||
// we mark dirty if the client is transitioning to always-on
|
||||
markDirty := false
|
||||
var becameAlwaysOn, autoreplayMissedDisabled bool
|
||||
alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
|
||||
client.stateMutex.Lock()
|
||||
client.accountSettings = settings
|
||||
if client.registered {
|
||||
markDirty = !client.alwaysOn && alwaysOn
|
||||
// only allow the client to become always-on if their nick equals their account name
|
||||
alwaysOn = alwaysOn && client.nick == client.accountName
|
||||
autoreplayMissedDisabled = (client.accountSettings.AutoreplayMissed && !settings.AutoreplayMissed)
|
||||
becameAlwaysOn = (!client.alwaysOn && alwaysOn)
|
||||
client.alwaysOn = alwaysOn
|
||||
if autoreplayMissedDisabled {
|
||||
// clear the lastSeen entry for the default session, but not for device IDs
|
||||
delete(client.lastSeen, "")
|
||||
}
|
||||
}
|
||||
client.accountSettings = settings
|
||||
client.stateMutex.Unlock()
|
||||
if markDirty {
|
||||
if becameAlwaysOn {
|
||||
client.markDirty(IncludeAllAttrs)
|
||||
} else if autoreplayMissedDisabled {
|
||||
client.markDirty(IncludeLastSeen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,7 +369,11 @@ func (client *Client) SetMode(mode modes.Mode, on bool) bool {
|
||||
func (client *Client) SetRealname(realname string) {
|
||||
client.stateMutex.Lock()
|
||||
client.realname = realname
|
||||
alwaysOn := client.registered && client.alwaysOn
|
||||
client.stateMutex.Unlock()
|
||||
if alwaysOn {
|
||||
client.markDirty(IncludeRealname)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) Channels() (result []*Channel) {
|
||||
@@ -408,6 +418,21 @@ func (client *Client) detailsNoMutex() (result ClientDetails) {
|
||||
return
|
||||
}
|
||||
|
||||
func (client *Client) UpdateActive(session *Session) {
|
||||
now := time.Now().UTC()
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
client.lastActive = now
|
||||
session.lastActive = now
|
||||
}
|
||||
|
||||
func (client *Client) Realname() string {
|
||||
client.stateMutex.RLock()
|
||||
result := client.realname
|
||||
client.stateMutex.RUnlock()
|
||||
return result
|
||||
}
|
||||
|
||||
func (channel *Channel) Name() string {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
@@ -423,9 +448,11 @@ func (channel *Channel) NameCasefolded() string {
|
||||
func (channel *Channel) Rename(name, nameCasefolded string) {
|
||||
channel.stateMutex.Lock()
|
||||
channel.name = name
|
||||
channel.nameCasefolded = nameCasefolded
|
||||
if channel.registeredFounder != "" {
|
||||
channel.registeredTime = time.Now().UTC()
|
||||
if channel.nameCasefolded != nameCasefolded {
|
||||
channel.nameCasefolded = nameCasefolded
|
||||
if channel.registeredFounder != "" {
|
||||
channel.registeredTime = time.Now().UTC()
|
||||
}
|
||||
}
|
||||
channel.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
+419
-100
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/history"
|
||||
"github.com/oragono/oragono/irc/jwt"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
@@ -82,7 +83,7 @@ func sendSuccessfulRegResponse(client *Client, rb *ResponseBuffer, forNS bool) {
|
||||
} else {
|
||||
rb.Add(nil, client.server.name, RPL_REG_SUCCESS, details.nick, details.accountName, client.t("Account created"))
|
||||
}
|
||||
client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] registered account $c[grey][$r%s$c[grey]]"), details.nickMask, details.accountName))
|
||||
client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] registered account $c[grey][$r%s$c[grey]] from IP %s"), details.nickMask, details.accountName, rb.session.IP().String()))
|
||||
sendSuccessfulAccountAuth(client, rb, forNS, false)
|
||||
}
|
||||
|
||||
@@ -236,6 +237,15 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
|
||||
return false
|
||||
}
|
||||
|
||||
// see #843: strip the device ID for the benefit of clients that don't
|
||||
// distinguish user/ident from account name
|
||||
if strudelIndex := strings.IndexByte(authcid, '@'); strudelIndex != -1 {
|
||||
var deviceID string
|
||||
authcid, deviceID = authcid[:strudelIndex], authcid[strudelIndex+1:]
|
||||
if !client.registered {
|
||||
rb.session.deviceID = deviceID
|
||||
}
|
||||
}
|
||||
password := string(splitValue[2])
|
||||
err := server.accounts.AuthenticateByPassphrase(client, authcid, password)
|
||||
if err != nil {
|
||||
@@ -251,6 +261,10 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
|
||||
}
|
||||
|
||||
func authErrorToMessage(server *Server, err error) (msg string) {
|
||||
if throttled, ok := err.(*ThrottleError); ok {
|
||||
return throttled.Error()
|
||||
}
|
||||
|
||||
switch err {
|
||||
case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch, errNickAccountMismatch:
|
||||
return err.Error()
|
||||
@@ -280,6 +294,15 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
// see #843: strip the device ID for the benefit of clients that don't
|
||||
// distinguish user/ident from account name
|
||||
if strudelIndex := strings.IndexByte(authzid, '@'); strudelIndex != -1 {
|
||||
var deviceID string
|
||||
authzid, deviceID = authzid[:strudelIndex], authzid[strudelIndex+1:]
|
||||
if !client.registered {
|
||||
rb.session.deviceID = deviceID
|
||||
}
|
||||
}
|
||||
err = server.accounts.AuthenticateByCertFP(client, rb.session.certfp, authzid)
|
||||
}
|
||||
|
||||
@@ -325,9 +348,9 @@ func dispatchAwayNotify(client *Client, isAway bool, awayMessage string) {
|
||||
details := client.Details()
|
||||
for session := range client.Friends(caps.AwayNotify) {
|
||||
if isAway {
|
||||
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.account, nil, "AWAY", awayMessage)
|
||||
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, nil, "AWAY", awayMessage)
|
||||
} else {
|
||||
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.account, nil, "AWAY")
|
||||
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, nil, "AWAY")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -736,6 +759,21 @@ func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
return false
|
||||
}
|
||||
|
||||
func defconHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
if len(msg.Params) > 0 {
|
||||
level, err := strconv.Atoi(msg.Params[0])
|
||||
if err == nil && 1 <= level && level <= 5 {
|
||||
server.SetDefcon(uint32(level))
|
||||
server.snomasks.Send(sno.LocalAnnouncements, fmt.Sprintf("%s [%s] set DEFCON level to %d", client.Nick(), client.Oper().Name, level))
|
||||
} else {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), msg.Command, client.t("Invalid DEFCON parameter"))
|
||||
return false
|
||||
}
|
||||
}
|
||||
rb.Notice(fmt.Sprintf(client.t("Current DEFCON level is %d"), server.Defcon()))
|
||||
return false
|
||||
}
|
||||
|
||||
// helper for parsing the reason args to DLINE and KLINE
|
||||
func getReasonsFromParams(params []string, currentArg int) (reason, operReason string) {
|
||||
reason = "No reason given"
|
||||
@@ -829,7 +867,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
return false
|
||||
}
|
||||
|
||||
if !dlineMyself && hostNet.Contains(client.IP()) {
|
||||
if !dlineMyself && hostNet.Contains(rb.session.IP()) {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("This ban matches you. To DLINE yourself, you must use the command: /DLINE MYSELF <arguments>"))
|
||||
return false
|
||||
}
|
||||
@@ -868,24 +906,30 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
|
||||
var killClient bool
|
||||
if andKill {
|
||||
var clientsToKill []*Client
|
||||
var sessionsToKill []*Session
|
||||
var killedClientNicks []string
|
||||
|
||||
for _, mcl := range server.clients.AllClients() {
|
||||
if hostNet.Contains(mcl.IP()) {
|
||||
clientsToKill = append(clientsToKill, mcl)
|
||||
killedClientNicks = append(killedClientNicks, mcl.nick)
|
||||
nickKilled := false
|
||||
for _, session := range mcl.Sessions() {
|
||||
if hostNet.Contains(session.IP()) {
|
||||
sessionsToKill = append(sessionsToKill, session)
|
||||
if !nickKilled {
|
||||
killedClientNicks = append(killedClientNicks, mcl.Nick())
|
||||
nickKilled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, mcl := range clientsToKill {
|
||||
mcl.SetExitedSnomaskSent()
|
||||
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
|
||||
if mcl == client {
|
||||
for _, session := range sessionsToKill {
|
||||
mcl := session.client
|
||||
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), session)
|
||||
if session == rb.session {
|
||||
killClient = true
|
||||
} else {
|
||||
// if mcl == client, we kill them below
|
||||
mcl.destroy(nil)
|
||||
mcl.destroy(session)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -897,6 +941,73 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
return killClient
|
||||
}
|
||||
|
||||
// EXTJWT <target> [service_name]
|
||||
func extjwtHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
accountName := client.AccountName()
|
||||
if accountName == "*" {
|
||||
accountName = ""
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"iss": server.name,
|
||||
"sub": client.Nick(),
|
||||
"account": accountName,
|
||||
"umodes": []string{},
|
||||
}
|
||||
|
||||
if msg.Params[0] != "*" {
|
||||
channel := server.channels.Get(msg.Params[0])
|
||||
if channel == nil {
|
||||
rb.Add(nil, server.name, "FAIL", "EXTJWT", "NO_SUCH_CHANNEL", client.t("No such channel"))
|
||||
return false
|
||||
}
|
||||
|
||||
claims["channel"] = channel.Name()
|
||||
claims["joined"] = 0
|
||||
claims["cmodes"] = []string{}
|
||||
if present, cModes := channel.ClientStatus(client); present {
|
||||
claims["joined"] = 1
|
||||
var modeStrings []string
|
||||
for _, cMode := range cModes {
|
||||
modeStrings = append(modeStrings, string(cMode))
|
||||
}
|
||||
claims["cmodes"] = modeStrings
|
||||
}
|
||||
}
|
||||
|
||||
config := server.Config()
|
||||
var serviceName string
|
||||
var sConfig jwt.JwtServiceConfig
|
||||
if 1 < len(msg.Params) {
|
||||
serviceName = strings.ToLower(msg.Params[1])
|
||||
sConfig = config.Extjwt.Services[serviceName]
|
||||
} else {
|
||||
serviceName = "*"
|
||||
sConfig = config.Extjwt.Default
|
||||
}
|
||||
|
||||
if !sConfig.Enabled() {
|
||||
rb.Add(nil, server.name, "FAIL", "EXTJWT", "NO_SUCH_SERVICE", client.t("No such service"))
|
||||
return false
|
||||
}
|
||||
|
||||
tokenString, err := sConfig.Sign(claims)
|
||||
|
||||
if err == nil {
|
||||
maxTokenLength := 400
|
||||
|
||||
for maxTokenLength < len(tokenString) {
|
||||
rb.Add(nil, server.name, "EXTJWT", msg.Params[0], serviceName, "*", tokenString[:maxTokenLength])
|
||||
tokenString = tokenString[maxTokenLength:]
|
||||
}
|
||||
rb.Add(nil, server.name, "EXTJWT", msg.Params[0], serviceName, tokenString)
|
||||
} else {
|
||||
rb.Add(nil, server.name, "FAIL", "EXTJWT", "UNKNOWN_ERROR", client.t("Could not generate EXTJWT token"))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HELP [<query>]
|
||||
func helpHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
argument := strings.ToLower(strings.TrimSpace(strings.Join(msg.Params, " ")))
|
||||
@@ -973,6 +1084,7 @@ func infoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
if Commit != "" {
|
||||
rb.Add(nil, server.name, RPL_INFO, nick, fmt.Sprintf(client.t("It was built from git hash %s."), Commit))
|
||||
}
|
||||
rb.Add(nil, server.name, RPL_INFO, nick, fmt.Sprintf(client.t("It was compiled using %s."), runtime.Version()))
|
||||
rb.Add(nil, server.name, RPL_INFO, nick, "")
|
||||
rb.Add(nil, server.name, RPL_INFO, nick, client.t("Oragono is released under the MIT license."))
|
||||
rb.Add(nil, server.name, RPL_INFO, nick, "")
|
||||
@@ -1052,16 +1164,10 @@ func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
keys = strings.Split(msg.Params[1], ",")
|
||||
}
|
||||
|
||||
config := server.Config()
|
||||
oper := client.Oper()
|
||||
for i, name := range channels {
|
||||
if name == "" {
|
||||
continue // #679
|
||||
}
|
||||
if config.Channels.MaxChannelsPerClient <= client.NumChannels() && oper == nil {
|
||||
rb.Add(nil, server.name, ERR_TOOMANYCHANNELS, client.Nick(), name, client.t("You have joined too many channels"))
|
||||
return false
|
||||
}
|
||||
var key string
|
||||
if len(keys) > i {
|
||||
key = keys[i]
|
||||
@@ -1075,18 +1181,35 @@ func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
}
|
||||
|
||||
func sendJoinError(client *Client, name string, rb *ResponseBuffer, err error) {
|
||||
var errMsg string
|
||||
var code, errMsg, forbiddingMode string
|
||||
switch err {
|
||||
case errInsufficientPrivs:
|
||||
errMsg = `Only server operators can create new channels`
|
||||
code, errMsg = ERR_NOSUCHCHANNEL, `Only server operators can create new channels`
|
||||
case errConfusableIdentifier:
|
||||
errMsg = `That channel name is too close to the name of another channel`
|
||||
code, errMsg = ERR_NOSUCHCHANNEL, `That channel name is too close to the name of another channel`
|
||||
case errChannelPurged:
|
||||
errMsg = err.Error()
|
||||
code, errMsg = ERR_NOSUCHCHANNEL, err.Error()
|
||||
case errTooManyChannels:
|
||||
code, errMsg = ERR_TOOMANYCHANNELS, `You have joined too many channels`
|
||||
case errLimitExceeded:
|
||||
code, forbiddingMode = ERR_CHANNELISFULL, "l"
|
||||
case errWrongChannelKey:
|
||||
code, forbiddingMode = ERR_BADCHANNELKEY, "k"
|
||||
case errInviteOnly:
|
||||
code, forbiddingMode = ERR_INVITEONLYCHAN, "i"
|
||||
case errBanned:
|
||||
code, forbiddingMode = ERR_BANNEDFROMCHAN, "b"
|
||||
case errRegisteredOnly:
|
||||
code, errMsg = ERR_NEEDREGGEDNICK, `You must be registered to join that channel`
|
||||
default:
|
||||
errMsg = `No such channel`
|
||||
code, errMsg = ERR_NOSUCHCHANNEL, `No such channel`
|
||||
}
|
||||
rb.Add(nil, client.server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(name), client.t(errMsg))
|
||||
if forbiddingMode != "" {
|
||||
errMsg = fmt.Sprintf(client.t("Cannot join channel (+%s)"), forbiddingMode)
|
||||
} else {
|
||||
errMsg = client.t(errMsg)
|
||||
}
|
||||
rb.Add(nil, client.server.name, code, client.Nick(), utils.SafeErrorParam(name), errMsg)
|
||||
}
|
||||
|
||||
// SAJOIN [nick] #channel{,#channel}
|
||||
@@ -1180,14 +1303,15 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
|
||||
target := server.clients.Get(nickname)
|
||||
if target == nil {
|
||||
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.nick, utils.SafeErrorParam(nickname), client.t("No such nick"))
|
||||
rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.Nick(), utils.SafeErrorParam(nickname), client.t("No such nick"))
|
||||
return false
|
||||
} else if target.AlwaysOn() {
|
||||
rb.Add(nil, client.server.name, ERR_UNKNOWNERROR, client.Nick(), "KILL", fmt.Sprintf(client.t("Client %s is always-on and cannot be fully removed by /KILL; consider /NS SUSPEND instead"), target.Nick()))
|
||||
}
|
||||
|
||||
quitMsg := fmt.Sprintf("Killed (%s (%s))", client.nick, comment)
|
||||
|
||||
server.snomasks.Send(sno.LocalKills, fmt.Sprintf(ircfmt.Unescape("%s$r was killed by %s $c[grey][$r%s$c[grey]]"), target.nick, client.nick, comment))
|
||||
target.SetExitedSnomaskSent()
|
||||
|
||||
target.Quit(quitMsg, nil)
|
||||
target.destroy(nil)
|
||||
@@ -1319,7 +1443,6 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
}
|
||||
|
||||
for _, mcl := range clientsToKill {
|
||||
mcl.SetExitedSnomaskSent()
|
||||
mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
|
||||
if mcl == client {
|
||||
killClient = true
|
||||
@@ -1440,6 +1563,13 @@ func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
}
|
||||
}
|
||||
|
||||
nick := client.Nick()
|
||||
rplList := func(channel *Channel) {
|
||||
if members, name, topic := channel.listData(); members != 0 {
|
||||
rb.Add(nil, client.server.name, RPL_LIST, nick, name, strconv.Itoa(members), topic)
|
||||
}
|
||||
}
|
||||
|
||||
clientIsOp := client.HasMode(modes.Operator)
|
||||
if len(channels) == 0 {
|
||||
for _, channel := range server.channels.Channels() {
|
||||
@@ -1447,7 +1577,7 @@ func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
continue
|
||||
}
|
||||
if matcher.Matches(channel) {
|
||||
client.RplList(channel, rb)
|
||||
rplList(channel)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1465,7 +1595,7 @@ func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
continue
|
||||
}
|
||||
if matcher.Matches(channel) {
|
||||
client.RplList(channel, rb)
|
||||
rplList(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1632,11 +1762,7 @@ func monitorRemoveHandler(server *Server, client *Client, msg ircmsg.IrcMessage,
|
||||
|
||||
targets := strings.Split(msg.Params[1], ",")
|
||||
for _, target := range targets {
|
||||
cfnick, err := CasefoldName(target)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
server.monitorManager.Remove(client, cfnick)
|
||||
server.monitorManager.Remove(rb.session, target)
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -1662,12 +1788,7 @@ func monitorAddHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb
|
||||
}
|
||||
|
||||
// add target
|
||||
casefoldedTarget, err := CasefoldName(target)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err = server.monitorManager.Add(client, casefoldedTarget, limits.MonitorEntries)
|
||||
err := server.monitorManager.Add(rb.session, target, limits.MonitorEntries)
|
||||
if err == errMonitorLimitExceeded {
|
||||
rb.Add(nil, server.name, ERR_MONLISTFULL, client.Nick(), strconv.Itoa(limits.MonitorEntries), strings.Join(targets, ","))
|
||||
break
|
||||
@@ -1696,14 +1817,14 @@ func monitorAddHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb
|
||||
|
||||
// MONITOR C
|
||||
func monitorClearHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
server.monitorManager.RemoveAll(client)
|
||||
server.monitorManager.RemoveAll(rb.session)
|
||||
return false
|
||||
}
|
||||
|
||||
// MONITOR L
|
||||
func monitorListHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
nick := client.Nick()
|
||||
monitorList := server.monitorManager.List(client)
|
||||
monitorList := server.monitorManager.List(rb.session)
|
||||
|
||||
var nickList []string
|
||||
for _, cfnick := range monitorList {
|
||||
@@ -1730,7 +1851,7 @@ func monitorStatusHandler(server *Server, client *Client, msg ircmsg.IrcMessage,
|
||||
var online []string
|
||||
var offline []string
|
||||
|
||||
monitorList := server.monitorManager.List(client)
|
||||
monitorList := server.monitorManager.List(rb.session)
|
||||
|
||||
for _, name := range monitorList {
|
||||
currentNick := server.getCurrentNick(name)
|
||||
@@ -1874,7 +1995,12 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
return false
|
||||
}
|
||||
|
||||
if rb.session.isTor && utils.IsRestrictedCTCPMessage(message) {
|
||||
isCTCP := utils.IsRestrictedCTCPMessage(message)
|
||||
if histType == history.Privmsg && !isCTCP {
|
||||
client.UpdateActive(rb.session)
|
||||
}
|
||||
|
||||
if rb.session.isTor && isCTCP {
|
||||
// note that error replies are never sent for NOTICE
|
||||
if histType != history.Notice {
|
||||
rb.Notice(client.t("CTCP messages are disabled over Tor"))
|
||||
@@ -1935,18 +2061,17 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
service, isService := OragonoServices[lowercaseTarget]
|
||||
_, isZNC := zncHandlers[lowercaseTarget]
|
||||
|
||||
if histType == history.Privmsg {
|
||||
if isService || isZNC {
|
||||
details := client.Details()
|
||||
rb.addEchoMessage(tags, details.nickMask, details.accountName, command, target, message)
|
||||
if histType != history.Privmsg {
|
||||
return // NOTICE and TAGMSG to services are ignored
|
||||
}
|
||||
if isService {
|
||||
servicePrivmsgHandler(service, server, client, message.Message, rb)
|
||||
return
|
||||
} else if isZNC {
|
||||
zncPrivmsgHandler(client, lowercaseTarget, message.Message, rb)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// NOTICE and TAGMSG to services are ignored
|
||||
if isService || isZNC {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1957,10 +2082,20 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Restrict CTCP message for target user with +T
|
||||
if user.modes.HasMode(modes.UserNoCTCP) && message.IsRestrictedCTCPMessage() {
|
||||
return
|
||||
}
|
||||
|
||||
tDetails := user.Details()
|
||||
tnick := tDetails.nick
|
||||
|
||||
details := client.Details()
|
||||
if details.account == "" && server.Defcon() <= 3 {
|
||||
rb.Add(nil, server.name, ERR_NEEDREGGEDNICK, client.Nick(), tnick, client.t("Direct messages from unregistered users are temporarily restricted"))
|
||||
return
|
||||
}
|
||||
nickMaskString := details.nickMask
|
||||
accountName := details.accountName
|
||||
var deliverySessions []*Session
|
||||
@@ -1994,21 +2129,12 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
}
|
||||
|
||||
// the originating session may get an echo message:
|
||||
if rb.session.capabilities.Has(caps.EchoMessage) {
|
||||
hasTagsCap := rb.session.capabilities.Has(caps.MessageTags)
|
||||
if histType == history.Tagmsg && hasTagsCap {
|
||||
rb.AddFromClient(message.Time, message.Msgid, nickMaskString, accountName, tags, command, tnick)
|
||||
} else {
|
||||
tagsToSend := tags
|
||||
if !hasTagsCap {
|
||||
tagsToSend = nil
|
||||
}
|
||||
rb.AddSplitMessageFromClient(nickMaskString, accountName, tagsToSend, command, tnick, message)
|
||||
}
|
||||
}
|
||||
if histType != history.Notice && user.Away() {
|
||||
rb.addEchoMessage(tags, nickMaskString, accountName, command, tnick, message)
|
||||
if histType != history.Notice {
|
||||
//TODO(dan): possibly implement cooldown of away notifications to users
|
||||
rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, user.AwayMessage())
|
||||
if away, awayMessage := user.Away(); away {
|
||||
rb.Add(nil, server.name, RPL_AWAY, client.Nick(), tnick, awayMessage)
|
||||
}
|
||||
}
|
||||
|
||||
config := server.Config()
|
||||
@@ -2022,7 +2148,7 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
AccountName: accountName,
|
||||
Tags: tags,
|
||||
}
|
||||
if !item.IsStorable() || !allowedPlusR {
|
||||
if !itemIsStorable(&item, config) || !allowedPlusR {
|
||||
return
|
||||
}
|
||||
targetedItem := item
|
||||
@@ -2045,6 +2171,32 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
}
|
||||
}
|
||||
|
||||
func itemIsStorable(item *history.Item, config *Config) bool {
|
||||
switch item.Type {
|
||||
case history.Tagmsg:
|
||||
if config.History.TagmsgStorage.Default {
|
||||
for _, blacklistedTag := range config.History.TagmsgStorage.Blacklist {
|
||||
if _, ok := item.Tags[blacklistedTag]; ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
for _, whitelistedTag := range config.History.TagmsgStorage.Whitelist {
|
||||
if _, ok := item.Tags[whitelistedTag]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
case history.Privmsg, history.Notice:
|
||||
// don't store CTCP other than ACTION
|
||||
return !item.Message.IsRestrictedCTCPMessage()
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// NPC <target> <sourcenick> <message>
|
||||
func npcHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
target := msg.Params[0]
|
||||
@@ -2093,8 +2245,8 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
var checkPassed, checkFailed, passwordFailed bool
|
||||
oper := server.GetOperator(msg.Params[0])
|
||||
if oper != nil {
|
||||
if oper.Fingerprint != "" {
|
||||
if oper.Fingerprint == rb.session.certfp {
|
||||
if oper.Certfp != "" {
|
||||
if oper.Certfp == rb.session.certfp {
|
||||
checkPassed = true
|
||||
} else {
|
||||
checkFailed = true
|
||||
@@ -2199,8 +2351,8 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
rb.Add(nil, server.name, ERR_ALREADYREGISTRED, client.nick, client.t("You may not reregister"))
|
||||
return false
|
||||
}
|
||||
// only give them one try to run the PASS command (all code paths end with this
|
||||
// variable being set):
|
||||
// only give them one try to run the PASS command (if a server password is set,
|
||||
// then all code paths end with this variable being set):
|
||||
if rb.session.passStatus != serverPassUnsent {
|
||||
return false
|
||||
}
|
||||
@@ -2211,18 +2363,17 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
if config.Accounts.LoginViaPassCommand {
|
||||
colonIndex := strings.IndexByte(password, ':')
|
||||
if colonIndex != -1 && client.Account() == "" {
|
||||
// TODO consolidate all login throttle checks into AccountManager
|
||||
throttled, _ := client.loginThrottle.Touch()
|
||||
if !throttled {
|
||||
account, accountPass := password[:colonIndex], password[colonIndex+1:]
|
||||
err := server.accounts.AuthenticateByPassphrase(client, account, accountPass)
|
||||
if err == nil {
|
||||
sendSuccessfulAccountAuth(client, rb, false, true)
|
||||
// login-via-pass-command entails that we do not need to check
|
||||
// an actual server password (either no password or skip-server-password)
|
||||
rb.session.passStatus = serverPassSuccessful
|
||||
return false
|
||||
}
|
||||
account, accountPass := password[:colonIndex], password[colonIndex+1:]
|
||||
if strudelIndex := strings.IndexByte(account, '@'); strudelIndex != -1 {
|
||||
account, rb.session.deviceID = account[:strudelIndex], account[strudelIndex+1:]
|
||||
}
|
||||
err := server.accounts.AuthenticateByPassphrase(client, account, accountPass)
|
||||
if err == nil {
|
||||
sendSuccessfulAccountAuth(client, rb, false, true)
|
||||
// login-via-pass-command entails that we do not need to check
|
||||
// an actual server password (either no password or skip-server-password)
|
||||
rb.session.passStatus = serverPassSuccessful
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2251,7 +2402,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
|
||||
// PING [params...]
|
||||
func pingHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
rb.Add(nil, server.name, "PONG", msg.Params...)
|
||||
rb.Add(nil, server.name, "PONG", server.name, msg.Params[0])
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2351,8 +2502,7 @@ func relaymsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *
|
||||
}
|
||||
|
||||
// RENAME <oldchan> <newchan> [<reason>]
|
||||
func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (result bool) {
|
||||
result = false
|
||||
func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
oldName, newName := msg.Params[0], msg.Params[1]
|
||||
var reason string
|
||||
if 2 < len(msg.Params) {
|
||||
@@ -2364,6 +2514,8 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(oldName), client.t("No such channel"))
|
||||
return false
|
||||
}
|
||||
oldName = channel.Name()
|
||||
|
||||
if !(channel.ClientIsAtLeast(client, modes.ChannelOperator) || client.HasRoleCapabs("chanreg")) {
|
||||
rb.Add(nil, server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), oldName, client.t("You're not a channel operator"))
|
||||
return false
|
||||
@@ -2371,14 +2523,14 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
|
||||
founder := channel.Founder()
|
||||
if founder != "" && founder != client.Account() {
|
||||
rb.Add(nil, server.name, ERR_CANNOTRENAME, client.Nick(), oldName, newName, client.t("Only channel founders can change registered channels"))
|
||||
rb.Add(nil, server.name, "FAIL", "RENAME", "CANNOT_RENAME", oldName, utils.SafeErrorParam(newName), client.t("Only channel founders can change registered channels"))
|
||||
return false
|
||||
}
|
||||
|
||||
config := server.Config()
|
||||
status, _ := channel.historyStatus(config)
|
||||
if status == HistoryPersistent {
|
||||
rb.Add(nil, server.name, ERR_CANNOTRENAME, client.Nick(), oldName, newName, client.t("Channels with persistent history cannot be renamed"))
|
||||
rb.Add(nil, server.name, "FAIL", "RENAME", "CANNOT_RENAME", oldName, utils.SafeErrorParam(newName), client.t("Channels with persistent history cannot be renamed"))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2387,9 +2539,9 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
if err == errInvalidChannelName {
|
||||
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(newName), client.t(err.Error()))
|
||||
} else if err == errChannelNameInUse {
|
||||
rb.Add(nil, server.name, ERR_CHANNAMEINUSE, client.Nick(), utils.SafeErrorParam(newName), client.t(err.Error()))
|
||||
rb.Add(nil, server.name, "FAIL", "RENAME", "CHANNEL_NAME_IN_USE", oldName, utils.SafeErrorParam(newName), client.t(err.Error()))
|
||||
} else if err != nil {
|
||||
rb.Add(nil, server.name, ERR_CANNOTRENAME, client.Nick(), oldName, utils.SafeErrorParam(newName), client.t("Cannot rename channel"))
|
||||
rb.Add(nil, server.name, "FAIL", "RENAME", "CANNOT_RENAME", oldName, utils.SafeErrorParam(newName), client.t("Cannot rename channel"))
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -2485,13 +2637,18 @@ func sceneHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
// SETNAME <realname>
|
||||
func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
realname := msg.Params[0]
|
||||
if realname == "" {
|
||||
rb.Add(nil, server.name, "FAIL", "SETNAME", "INVALID_REALNAME", client.t("Realname is not valid"))
|
||||
return false
|
||||
}
|
||||
|
||||
client.SetRealname(realname)
|
||||
details := client.Details()
|
||||
|
||||
// alert friends
|
||||
now := time.Now().UTC()
|
||||
for session := range client.Friends(caps.SetName) {
|
||||
session.sendFromClientInternal(false, now, "", details.nickMask, details.account, nil, "SETNAME", details.realname)
|
||||
session.sendFromClientInternal(false, now, "", details.nickMask, details.accountName, nil, "SETNAME", details.realname)
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -2601,6 +2758,22 @@ func userHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
return false
|
||||
}
|
||||
|
||||
// #843: we accept either: `USER user:pass@clientid` or `USER user@clientid`
|
||||
if strudelIndex := strings.IndexByte(username, '@'); strudelIndex != -1 {
|
||||
username, rb.session.deviceID = username[:strudelIndex], username[strudelIndex+1:]
|
||||
if colonIndex := strings.IndexByte(username, ':'); colonIndex != -1 {
|
||||
var password string
|
||||
username, password = username[:colonIndex], username[colonIndex+1:]
|
||||
err := server.accounts.AuthenticateByPassphrase(client, username, password)
|
||||
if err == nil {
|
||||
sendSuccessfulAccountAuth(client, rb, false, true)
|
||||
} else {
|
||||
// this is wrong, but send something for debugging that will show up in a raw transcript
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), client.t("SASL authentication failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := client.SetNames(username, realname, false)
|
||||
if err == errInvalidUsername {
|
||||
// if client's using a unicode nick or something weird, let's just set 'em up with a stock username instead.
|
||||
@@ -2641,7 +2814,7 @@ func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *
|
||||
if target.HasMode(modes.Operator) {
|
||||
isOper = "*"
|
||||
}
|
||||
if target.Away() {
|
||||
if away, _ := target.Away(); away {
|
||||
isAway = "-"
|
||||
} else {
|
||||
isAway = "+"
|
||||
@@ -2712,7 +2885,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
|
||||
continue
|
||||
}
|
||||
if info.Fingerprint != "" && info.Fingerprint != rb.session.certfp {
|
||||
if info.Certfp != "" && info.Certfp != rb.session.certfp {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -2730,7 +2903,122 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
return true
|
||||
}
|
||||
|
||||
// WHO [<mask> [o]]
|
||||
type whoxFields uint32 // bitset to hold the WHOX field values, 'a' through 'z'
|
||||
|
||||
func (fields whoxFields) Add(field rune) (result whoxFields) {
|
||||
index := int(field) - int('a')
|
||||
if 0 <= index && index < 26 {
|
||||
return fields | (1 << index)
|
||||
} else {
|
||||
return fields
|
||||
}
|
||||
}
|
||||
|
||||
func (fields whoxFields) Has(field rune) bool {
|
||||
index := int(field) - int('a')
|
||||
if 0 <= index && index < 26 {
|
||||
return (fields & (1 << index)) != 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// rplWhoReply returns the WHO(X) reply between one user and another channel/user.
|
||||
// who format:
|
||||
// <channel> <user> <host> <server> <nick> <H|G>[*][~|&|@|%|+][B] :<hopcount> <real name>
|
||||
// whox format:
|
||||
// <type> <channel> <user> <ip> <host> <server> <nick> <H|G>[*][~|&|@|%|+][B] <hops> <idle> <account> <rank> :<real name>
|
||||
func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *ResponseBuffer, isWhox bool, fields whoxFields, whoType string) {
|
||||
params := []string{client.Nick()}
|
||||
|
||||
details := target.Details()
|
||||
|
||||
if fields.Has('t') {
|
||||
params = append(params, whoType)
|
||||
}
|
||||
if fields.Has('c') {
|
||||
fChannel := "*"
|
||||
if channel != nil {
|
||||
fChannel = channel.name
|
||||
}
|
||||
params = append(params, fChannel)
|
||||
}
|
||||
if fields.Has('u') {
|
||||
params = append(params, details.username)
|
||||
}
|
||||
if fields.Has('i') {
|
||||
fIP := "255.255.255.255"
|
||||
if client.HasMode(modes.Operator) || client == target {
|
||||
// you can only see a target's IP if they're you or you're an oper
|
||||
fIP = target.IPString()
|
||||
}
|
||||
params = append(params, fIP)
|
||||
}
|
||||
if fields.Has('h') {
|
||||
params = append(params, details.hostname)
|
||||
}
|
||||
if fields.Has('s') {
|
||||
params = append(params, target.server.name)
|
||||
}
|
||||
if fields.Has('n') {
|
||||
params = append(params, details.nick)
|
||||
}
|
||||
if fields.Has('f') { // "flags" (away + oper state + channel status prefix + bot)
|
||||
var flags strings.Builder
|
||||
if away, _ := target.Away(); away {
|
||||
flags.WriteRune('G') // Gone
|
||||
} else {
|
||||
flags.WriteRune('H') // Here
|
||||
}
|
||||
|
||||
if target.HasMode(modes.Operator) {
|
||||
flags.WriteRune('*')
|
||||
}
|
||||
|
||||
if channel != nil {
|
||||
flags.WriteString(channel.ClientPrefixes(target, rb.session.capabilities.Has(caps.MultiPrefix)))
|
||||
}
|
||||
|
||||
if target.HasMode(modes.Bot) {
|
||||
flags.WriteRune('B')
|
||||
}
|
||||
|
||||
params = append(params, flags.String())
|
||||
|
||||
}
|
||||
if fields.Has('d') { // server hops from us to target
|
||||
params = append(params, "0")
|
||||
}
|
||||
if fields.Has('l') {
|
||||
params = append(params, fmt.Sprintf("%d", target.IdleSeconds()))
|
||||
}
|
||||
if fields.Has('a') {
|
||||
fAccount := "0"
|
||||
if details.accountName != "*" {
|
||||
// WHOX uses "0" to mean "no account"
|
||||
fAccount = details.accountName
|
||||
}
|
||||
params = append(params, fAccount)
|
||||
}
|
||||
if fields.Has('o') { // target's channel power level
|
||||
//TODO: implement this
|
||||
params = append(params, "0")
|
||||
}
|
||||
if fields.Has('r') {
|
||||
params = append(params, details.realname)
|
||||
}
|
||||
|
||||
numeric := RPL_WHOSPCRPL
|
||||
if !isWhox {
|
||||
numeric = RPL_WHOREPLY
|
||||
// if this isn't WHOX, stick hops + realname at the end
|
||||
params = append(params, "0 "+details.realname)
|
||||
}
|
||||
|
||||
rb.Add(nil, client.server.name, numeric, params...)
|
||||
}
|
||||
|
||||
// WHO <mask> [<filter>%<fields>,<type>]
|
||||
func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
mask := msg.Params[0]
|
||||
var err error
|
||||
@@ -2748,6 +3036,26 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
return false
|
||||
}
|
||||
|
||||
sFields := "cuhsnf"
|
||||
whoType := "0"
|
||||
isWhox := false
|
||||
if len(msg.Params) > 1 && strings.Contains(msg.Params[1], "%") {
|
||||
isWhox = true
|
||||
whoxData := msg.Params[1]
|
||||
fieldStart := strings.Index(whoxData, "%")
|
||||
sFields = whoxData[fieldStart+1:]
|
||||
|
||||
typeIndex := strings.Index(sFields, ",")
|
||||
if typeIndex > -1 && typeIndex < (len(sFields)-1) { // make sure there's , and a value after it
|
||||
whoType = sFields[typeIndex+1:]
|
||||
sFields = strings.ToLower(sFields[:typeIndex])
|
||||
}
|
||||
}
|
||||
var fields whoxFields
|
||||
for _, field := range sFields {
|
||||
fields = fields.Add(field)
|
||||
}
|
||||
|
||||
//TODO(dan): is this used and would I put this param in the Modern doc?
|
||||
// if not, can we remove it?
|
||||
//var operatorOnly bool
|
||||
@@ -2765,16 +3073,16 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
if !channel.flags.HasMode(modes.Secret) || isJoined || isOper {
|
||||
for _, member := range channel.Members() {
|
||||
if !member.HasMode(modes.Invisible) || isJoined || isOper {
|
||||
client.rplWhoReply(channel, member, rb)
|
||||
client.rplWhoReply(channel, member, rb, isWhox, fields, whoType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Construct set of channels the client is in.
|
||||
userChannels := make(map[*Channel]bool)
|
||||
userChannels := make(ChannelSet)
|
||||
for _, channel := range client.Channels() {
|
||||
userChannels[channel] = true
|
||||
userChannels[channel] = empty{}
|
||||
}
|
||||
|
||||
// Another client is a friend if they share at least one channel, or they are the same client.
|
||||
@@ -2784,7 +3092,7 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
}
|
||||
|
||||
for _, channel := range otherClient.Channels() {
|
||||
if userChannels[channel] {
|
||||
if _, present := userChannels[channel]; present {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -2793,7 +3101,7 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
||||
|
||||
for mclient := range server.clients.FindAll(mask) {
|
||||
if isOper || !mclient.HasMode(modes.Invisible) || isFriend(mclient) {
|
||||
client.rplWhoReply(nil, mclient, rb)
|
||||
client.rplWhoReply(nil, mclient, rb, isWhox, fields, whoType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2893,7 +3201,12 @@ func whowasHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
|
||||
// ZNC <module> [params]
|
||||
func zncHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
zncModuleHandler(client, msg.Params[0], msg.Params[1:], rb)
|
||||
params := msg.Params[1:]
|
||||
// #1205: compatibility with Palaver, which sends `ZNC *playback :play ...`
|
||||
if len(params) == 1 && strings.IndexByte(params[0], ' ') != -1 {
|
||||
params = strings.Fields(params[0])
|
||||
}
|
||||
zncModuleHandler(client, msg.Params[0], params, rb)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2902,3 +3215,9 @@ func unknownCommandHandler(server *Server, client *Client, msg ircmsg.IrcMessage
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNCOMMAND, client.Nick(), utils.SafeErrorParam(msg.Command), client.t("Unknown command"))
|
||||
return false
|
||||
}
|
||||
|
||||
// fake handler for invalid utf8
|
||||
func invalidUtf8Handler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
rb.Add(nil, server.name, "FAIL", utils.SafeErrorParam(msg.Command), "INVALID_UTF8", client.t("Message rejected for containing invalid UTF-8"))
|
||||
return false
|
||||
}
|
||||
|
||||
+26
-2
@@ -51,6 +51,8 @@ Oragono supports the following channel modes:
|
||||
+R | Only registered users can join the channel.
|
||||
+s | Secret mode, channel won't show up in /LIST or whois replies.
|
||||
+t | Only channel opers can modify the topic.
|
||||
+E | Roleplaying commands are enabled in the channel.
|
||||
+C | Clients are blocked from sending CTCP messages in the channel.
|
||||
|
||||
= Prefixes =
|
||||
|
||||
@@ -66,9 +68,12 @@ Oragono supports the following user modes:
|
||||
+a | User is marked as being away. This mode is set with the /AWAY command.
|
||||
+i | User is marked as invisible (their channels are hidden from whois replies).
|
||||
+o | User is an IRC operator.
|
||||
+R | User only accepts messages from other registered users.
|
||||
+R | User only accepts messages from other registered users.
|
||||
+s | Server Notice Masks (see help with /HELPOP snomasks).
|
||||
+Z | User is connected via TLS.`
|
||||
+Z | User is connected via TLS.
|
||||
+B | User is a bot.
|
||||
+E | User can receive roleplaying commands.
|
||||
+T | User is blocked from sending CTCP messages.`
|
||||
snomaskHelpText = `== Server Notice Masks ==
|
||||
|
||||
Oragono supports the following server notice masks for operators:
|
||||
@@ -162,6 +167,20 @@ Provides various debugging commands for the IRCd. <option> can be one of:
|
||||
* STOPCPUPROFILE: Stops the CPU profiler.
|
||||
* PROFILEHEAP: Writes a memory profile.
|
||||
* CRASHSERVER: Crashes the server (for use in failover testing)`,
|
||||
},
|
||||
"defcon": {
|
||||
oper: true,
|
||||
text: `DEFCON [level]
|
||||
|
||||
The DEFCON system can disable server features at runtime, to mitigate
|
||||
spam or other hostile activity. It has five levels, which are cumulative
|
||||
(i.e., level 3 includes all restrictions from level 4 and so on):
|
||||
|
||||
5: Normal operation
|
||||
4: No new account or channel registrations
|
||||
3: All users are +R; no changes to vhosts
|
||||
2: No new unauthenticated connections; all channels are +R
|
||||
1: No new connections except from localhost or other trusted IPs`,
|
||||
},
|
||||
"deoper": {
|
||||
oper: true,
|
||||
@@ -198,6 +217,11 @@ ON <server> specifies that the ban is to be set on that specific server.
|
||||
[reason] and [oper reason], if they exist, are separated by a vertical bar (|).
|
||||
|
||||
If "DLINE LIST" is sent, the server sends back a list of our current DLINEs.`,
|
||||
},
|
||||
"extjwt": {
|
||||
text: `EXTJWT <target> [service_name]
|
||||
|
||||
Get a JSON Web Token for target (either * or a channel name).`,
|
||||
},
|
||||
"help": {
|
||||
text: `HELP <argument>
|
||||
|
||||
@@ -29,12 +29,6 @@ const (
|
||||
initialAutoSize = 32
|
||||
)
|
||||
|
||||
// a Tagmsg that consists entirely of transient tags is not stored
|
||||
var transientTags = map[string]bool{
|
||||
"+draft/typing": true,
|
||||
"+typing": true, // future-proofing
|
||||
}
|
||||
|
||||
// Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data
|
||||
type Item struct {
|
||||
Type ItemType
|
||||
@@ -57,23 +51,6 @@ func (item *Item) HasMsgid(msgid string) bool {
|
||||
return item.Message.Msgid == msgid
|
||||
}
|
||||
|
||||
func (item *Item) IsStorable() bool {
|
||||
switch item.Type {
|
||||
case Tagmsg:
|
||||
for name := range item.Tags {
|
||||
if !transientTags[name] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false // all tags were blacklisted
|
||||
case Privmsg, Notice:
|
||||
// don't store CTCP other than ACTION
|
||||
return !item.Message.IsRestrictedCTCPMessage()
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
type Predicate func(item *Item) (matches bool)
|
||||
|
||||
func Reverse(results []Item) {
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Selector represents a parameter to a CHATHISTORY command;
|
||||
// at most one of Msgid or Time may be nonzero
|
||||
// Selector represents a parameter to a CHATHISTORY command
|
||||
type Selector struct {
|
||||
Msgid string
|
||||
Time time.Time
|
||||
|
||||
-116
@@ -129,51 +129,6 @@ for the rejection.`,
|
||||
maxParams: 2,
|
||||
unsplitFinalParam: true,
|
||||
},
|
||||
"forbid": {
|
||||
handler: hsForbidHandler,
|
||||
help: `Syntax: $bFORBID <user>$b
|
||||
|
||||
FORBID prevents a user from using any vhost, including ones on the offer list.`,
|
||||
helpShort: `$bFORBID$b prevents a user from using vhosts.`,
|
||||
capabs: []string{"vhosts"},
|
||||
enabled: hostservEnabled,
|
||||
minParams: 1,
|
||||
maxParams: 1,
|
||||
},
|
||||
"permit": {
|
||||
handler: hsForbidHandler,
|
||||
help: `Syntax: $bPERMIT <user>$b
|
||||
|
||||
PERMIT undoes FORBID, allowing the user to TAKE vhosts again.`,
|
||||
helpShort: `$bPERMIT$b allows a user to use vhosts again.`,
|
||||
capabs: []string{"vhosts"},
|
||||
enabled: hostservEnabled,
|
||||
minParams: 1,
|
||||
maxParams: 1,
|
||||
},
|
||||
"offerlist": {
|
||||
handler: hsOfferListHandler,
|
||||
help: `Syntax: $bOFFERLIST$b
|
||||
|
||||
OFFERLIST lists vhosts that can be chosen without requiring operator approval;
|
||||
to use one of the listed vhosts, take it with /HOSTSERV TAKE.`,
|
||||
helpShort: `$bOFFERLIST$b lists vhosts that can be taken without operator approval.`,
|
||||
enabled: hostservEnabled,
|
||||
minParams: 0,
|
||||
maxParams: 0,
|
||||
},
|
||||
"take": {
|
||||
handler: hsTakeHandler,
|
||||
help: `Syntax: $bTAKE$b <vhost>
|
||||
|
||||
TAKE sets your vhost to one of the vhosts in the server's offer list; to see
|
||||
the offered vhosts, use /HOSTSERV OFFERLIST.`,
|
||||
helpShort: `$bTAKE$b sets your vhost to one of the options from the offer list.`,
|
||||
enabled: hostservEnabled,
|
||||
authRequired: true,
|
||||
minParams: 1,
|
||||
maxParams: 1,
|
||||
},
|
||||
"setcloaksecret": {
|
||||
handler: hsSetCloakSecretHandler,
|
||||
help: `Syntax: $bSETCLOAKSECRET$b <secret> [code]
|
||||
@@ -238,8 +193,6 @@ func hsRequestHandler(server *Server, client *Client, command string, params []s
|
||||
if err != nil {
|
||||
if throttled, ok := err.(*vhostThrottleExceeded); ok {
|
||||
hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), throttled.timeRemaining))
|
||||
} else if err == errVhostsForbidden {
|
||||
hsNotice(rb, client.t("An administrator has denied you the ability to use vhosts"))
|
||||
} else {
|
||||
hsNotice(rb, client.t("An error occurred"))
|
||||
}
|
||||
@@ -276,11 +229,6 @@ func hsStatusHandler(server *Server, client *Client, command string, params []st
|
||||
return
|
||||
}
|
||||
|
||||
if account.VHost.Forbidden {
|
||||
hsNotice(rb, client.t("An administrator has denied you the ability to use vhosts"))
|
||||
return
|
||||
}
|
||||
|
||||
if account.VHost.ApprovedVHost != "" {
|
||||
hsNotice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost))
|
||||
if !account.VHost.Enabled {
|
||||
@@ -382,70 +330,6 @@ func hsRejectHandler(server *Server, client *Client, command string, params []st
|
||||
}
|
||||
}
|
||||
|
||||
func hsForbidHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
user := params[0]
|
||||
forbidden := command == "forbid"
|
||||
|
||||
_, err := server.accounts.VHostForbid(user, forbidden)
|
||||
if err == errAccountDoesNotExist {
|
||||
hsNotice(rb, client.t("No such account"))
|
||||
} else if err != nil {
|
||||
hsNotice(rb, client.t("An error occurred"))
|
||||
} else {
|
||||
if forbidden {
|
||||
hsNotice(rb, fmt.Sprintf(client.t("User %s is no longer allowed to use vhosts"), user))
|
||||
} else {
|
||||
hsNotice(rb, fmt.Sprintf(client.t("User %s is now allowed to use vhosts"), user))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hsOfferListHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
vhostConfig := server.Config().Accounts.VHosts
|
||||
if len(vhostConfig.OfferList) == 0 {
|
||||
if vhostConfig.UserRequests.Enabled {
|
||||
hsNotice(rb, client.t("The server does not offer any vhosts, but you can request one with /HOSTSERV REQUEST"))
|
||||
} else {
|
||||
hsNotice(rb, client.t("The server does not offer any vhosts"))
|
||||
}
|
||||
} else {
|
||||
hsNotice(rb, client.t("The following vhosts are available and can be chosen with /HOSTSERV TAKE:"))
|
||||
for _, vhost := range vhostConfig.OfferList {
|
||||
hsNotice(rb, vhost)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hsTakeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
config := server.Config()
|
||||
vhost := params[0]
|
||||
found := false
|
||||
for _, offered := range config.Accounts.VHosts.OfferList {
|
||||
if offered == vhost {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
hsNotice(rb, client.t("That vhost isn't being offered by the server"))
|
||||
return
|
||||
}
|
||||
|
||||
account := client.Account()
|
||||
_, err := server.accounts.VHostTake(account, vhost, time.Duration(config.Accounts.VHosts.UserRequests.Cooldown))
|
||||
if err != nil {
|
||||
if throttled, ok := err.(*vhostThrottleExceeded); ok {
|
||||
hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before taking a vhost"), throttled.timeRemaining))
|
||||
} else if err == errVhostsForbidden {
|
||||
hsNotice(rb, client.t("An administrator has denied you the ability to use vhosts"))
|
||||
} else {
|
||||
hsNotice(rb, client.t("An error occurred"))
|
||||
}
|
||||
} else {
|
||||
hsNotice(rb, client.t("Successfully set vhost"))
|
||||
server.snomasks.Send(sno.LocalVhosts, fmt.Sprintf("Client %s (account %s) took vhost %s", client.Nick(), account, vhost))
|
||||
}
|
||||
}
|
||||
|
||||
func hsSetCloakSecretHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
secret := params[0]
|
||||
expectedCode := utils.ConfirmationCode(secret, server.ctime)
|
||||
|
||||
@@ -4,179 +4,9 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
)
|
||||
|
||||
const (
|
||||
// RegisterTimeout is how long clients have to register before we disconnect them
|
||||
RegisterTimeout = time.Minute
|
||||
// DefaultIdleTimeout is how long without traffic before we send the client a PING
|
||||
DefaultIdleTimeout = time.Minute + 30*time.Second
|
||||
// For Tor clients, we send a PING at least every 30 seconds, as a workaround for this bug
|
||||
// (single-onion circuits will close unless the client sends data once every 60 seconds):
|
||||
// https://bugs.torproject.org/29665
|
||||
TorIdleTimeout = time.Second * 30
|
||||
// This is how long a client gets without sending any message, including the PONG to our
|
||||
// PING, before we disconnect them:
|
||||
DefaultTotalTimeout = 2*time.Minute + 30*time.Second
|
||||
// Resumeable clients (clients who have negotiated caps.Resume) get longer:
|
||||
ResumeableTotalTimeout = 3*time.Minute + 30*time.Second
|
||||
)
|
||||
|
||||
// client idleness state machine
|
||||
|
||||
type TimerState uint
|
||||
|
||||
const (
|
||||
TimerUnregistered TimerState = iota // client is unregistered
|
||||
TimerActive // client is actively sending commands
|
||||
TimerIdle // client is idle, we sent PING and are waiting for PONG
|
||||
TimerDead // client was terminated
|
||||
)
|
||||
|
||||
type IdleTimer struct {
|
||||
sync.Mutex // tier 1
|
||||
|
||||
// immutable after construction
|
||||
registerTimeout time.Duration
|
||||
session *Session
|
||||
|
||||
// mutable
|
||||
idleTimeout time.Duration
|
||||
quitTimeout time.Duration
|
||||
state TimerState
|
||||
timer *time.Timer
|
||||
}
|
||||
|
||||
// Initialize sets up an IdleTimer and starts counting idle time;
|
||||
// if there is no activity from the client, it will eventually be stopped.
|
||||
func (it *IdleTimer) Initialize(session *Session) {
|
||||
it.session = session
|
||||
it.registerTimeout = RegisterTimeout
|
||||
it.idleTimeout, it.quitTimeout = it.recomputeDurations()
|
||||
registered := session.client.Registered()
|
||||
|
||||
it.Lock()
|
||||
defer it.Unlock()
|
||||
if registered {
|
||||
it.state = TimerActive
|
||||
} else {
|
||||
it.state = TimerUnregistered
|
||||
}
|
||||
it.resetTimeout()
|
||||
}
|
||||
|
||||
// recomputeDurations recomputes the idle and quit durations, given the client's caps.
|
||||
func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duration) {
|
||||
totalTimeout := DefaultTotalTimeout
|
||||
// if they have the resume cap, wait longer before pinging them out
|
||||
// to give them a chance to resume their connection
|
||||
if it.session.capabilities.Has(caps.Resume) {
|
||||
totalTimeout = ResumeableTotalTimeout
|
||||
}
|
||||
|
||||
idleTimeout = DefaultIdleTimeout
|
||||
if it.session.isTor {
|
||||
idleTimeout = TorIdleTimeout
|
||||
}
|
||||
|
||||
quitTimeout = totalTimeout - idleTimeout
|
||||
return
|
||||
}
|
||||
|
||||
func (it *IdleTimer) Touch() {
|
||||
idleTimeout, quitTimeout := it.recomputeDurations()
|
||||
|
||||
it.Lock()
|
||||
defer it.Unlock()
|
||||
it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
|
||||
// a touch transitions TimerUnregistered or TimerIdle into TimerActive
|
||||
if it.state != TimerDead {
|
||||
it.state = TimerActive
|
||||
it.resetTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
func (it *IdleTimer) processTimeout() {
|
||||
idleTimeout, quitTimeout := it.recomputeDurations()
|
||||
|
||||
var previousState TimerState
|
||||
func() {
|
||||
it.Lock()
|
||||
defer it.Unlock()
|
||||
it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
|
||||
previousState = it.state
|
||||
// TimerActive transitions to TimerIdle, all others to TimerDead
|
||||
if it.state == TimerActive {
|
||||
// send them a ping, give them time to respond
|
||||
it.state = TimerIdle
|
||||
it.resetTimeout()
|
||||
} else {
|
||||
it.state = TimerDead
|
||||
}
|
||||
}()
|
||||
|
||||
if previousState == TimerActive {
|
||||
it.session.Ping()
|
||||
} else {
|
||||
it.session.client.Quit(it.quitMessage(previousState), it.session)
|
||||
it.session.client.destroy(it.session)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops counting idle time.
|
||||
func (it *IdleTimer) Stop() {
|
||||
if it == nil {
|
||||
return
|
||||
}
|
||||
|
||||
it.Lock()
|
||||
defer it.Unlock()
|
||||
it.state = TimerDead
|
||||
it.resetTimeout()
|
||||
}
|
||||
|
||||
func (it *IdleTimer) resetTimeout() {
|
||||
if it.timer != nil {
|
||||
it.timer.Stop()
|
||||
}
|
||||
var nextTimeout time.Duration
|
||||
switch it.state {
|
||||
case TimerUnregistered:
|
||||
nextTimeout = it.registerTimeout
|
||||
case TimerActive:
|
||||
nextTimeout = it.idleTimeout
|
||||
case TimerIdle:
|
||||
nextTimeout = it.quitTimeout
|
||||
case TimerDead:
|
||||
return
|
||||
}
|
||||
if it.timer != nil {
|
||||
it.timer.Reset(nextTimeout)
|
||||
} else {
|
||||
it.timer = time.AfterFunc(nextTimeout, it.processTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (it *IdleTimer) quitMessage(state TimerState) string {
|
||||
switch state {
|
||||
case TimerUnregistered:
|
||||
return fmt.Sprintf("Registration timeout: %v", it.registerTimeout)
|
||||
case TimerIdle:
|
||||
// how many seconds before registered clients are timed out (IdleTimeout plus QuitTimeout).
|
||||
it.Lock()
|
||||
defer it.Unlock()
|
||||
return fmt.Sprintf("Ping timeout: %v", (it.idleTimeout + it.quitTimeout))
|
||||
default:
|
||||
// shouldn't happen
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// BrbTimer is a timer on the client as a whole (not an individual session) for implementing
|
||||
// the BRB command and related functionality (where a client can remain online without
|
||||
// having any connected sessions).
|
||||
|
||||
+22
-13
@@ -3,6 +3,7 @@ package irc
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -13,11 +14,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxReadQBytes = ircmsg.MaxlenTagsFromClient + 512 + 1024
|
||||
maxReadQBytes = ircmsg.MaxlenTagsFromClient + MaxLineLen + 1024
|
||||
)
|
||||
|
||||
var (
|
||||
crlf = []byte{'\r', '\n'}
|
||||
crlf = []byte{'\r', '\n'}
|
||||
errReadQ = errors.New("ReadQ Exceeded")
|
||||
)
|
||||
|
||||
// IRCConn abstracts away the distinction between a regular
|
||||
@@ -31,7 +33,7 @@ type IRCConn interface {
|
||||
// these take an IRC line or lines, correctly terminated with CRLF:
|
||||
WriteLine([]byte) error
|
||||
WriteLines([][]byte) error
|
||||
// this returns an IRC line without the terminating CRLF:
|
||||
// this returns an IRC line, possibly terminated with CRLF, LF, or nothing:
|
||||
ReadLine() (line []byte, err error)
|
||||
|
||||
Close() error
|
||||
@@ -76,7 +78,9 @@ func (cc *IRCStreamConn) ReadLine() (line []byte, err error) {
|
||||
if isPrefix {
|
||||
return nil, errReadQ
|
||||
}
|
||||
line = bytes.TrimSuffix(line, crlf)
|
||||
if globalUtf8EnforcementSetting && !utf8.Valid(line) {
|
||||
err = errInvalidUtf8
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -101,9 +105,9 @@ func (wc IRCWSConn) UnderlyingConn() *utils.WrappedConn {
|
||||
|
||||
func (wc IRCWSConn) WriteLine(buf []byte) (err error) {
|
||||
buf = bytes.TrimSuffix(buf, crlf)
|
||||
// there's not much we can do about this;
|
||||
// silently drop the message
|
||||
if !utf8.Valid(buf) {
|
||||
if !globalUtf8EnforcementSetting && !utf8.Valid(buf) {
|
||||
// there's not much we can do about this;
|
||||
// silently drop the message
|
||||
return nil
|
||||
}
|
||||
return wc.conn.WriteMessage(websocket.TextMessage, buf)
|
||||
@@ -120,13 +124,18 @@ func (wc IRCWSConn) WriteLines(buffers [][]byte) (err error) {
|
||||
}
|
||||
|
||||
func (wc IRCWSConn) ReadLine() (line []byte, err error) {
|
||||
for {
|
||||
var messageType int
|
||||
messageType, line, err = wc.conn.ReadMessage()
|
||||
// on empty message or non-text message, try again, block if necessary
|
||||
if err != nil || (messageType == websocket.TextMessage && len(line) != 0) {
|
||||
return
|
||||
messageType, line, err := wc.conn.ReadMessage()
|
||||
if err == nil {
|
||||
if messageType == websocket.TextMessage {
|
||||
return line, nil
|
||||
} else {
|
||||
// for purposes of fakelag, treat non-text message as an empty line
|
||||
return nil, nil
|
||||
}
|
||||
} else if err == websocket.ErrReadLimit {
|
||||
return line, errReadQ
|
||||
} else {
|
||||
return line, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2020 Daniel Oaks <daniel@danieloaks.net>
|
||||
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoKeys = errors.New("No signing keys are enabled")
|
||||
)
|
||||
|
||||
type MapClaims jwt.MapClaims
|
||||
|
||||
type JwtServiceConfig struct {
|
||||
Expiration time.Duration
|
||||
Secret string
|
||||
secretBytes []byte
|
||||
RSAPrivateKeyFile string `yaml:"rsa-private-key-file"`
|
||||
rsaPrivateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (t *JwtServiceConfig) Postprocess() (err error) {
|
||||
t.secretBytes = []byte(t.Secret)
|
||||
t.Secret = ""
|
||||
if t.RSAPrivateKeyFile != "" {
|
||||
keyBytes, err := ioutil.ReadFile(t.RSAPrivateKeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, _ := pem.Decode(keyBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.rsaPrivateKey, err = x509.ParsePKCS1PrivateKey(d.Bytes)
|
||||
if err != nil {
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(d.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey); ok {
|
||||
t.rsaPrivateKey = rsaPrivateKey
|
||||
} else {
|
||||
return fmt.Errorf("Non-RSA key type for extjwt: %T", privateKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *JwtServiceConfig) Enabled() bool {
|
||||
return t.Expiration != 0 && (len(t.secretBytes) != 0 || t.rsaPrivateKey != nil)
|
||||
}
|
||||
|
||||
func (t *JwtServiceConfig) Sign(claims MapClaims) (result string, err error) {
|
||||
claims["exp"] = time.Now().Unix() + int64(t.Expiration/time.Second)
|
||||
|
||||
if t.rsaPrivateKey != nil {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(claims))
|
||||
return token.SignedString(t.rsaPrivateKey)
|
||||
} else if len(t.secretBytes) != 0 {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(claims))
|
||||
return token.SignedString(t.secretBytes)
|
||||
} else {
|
||||
return "", ErrNoKeys
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2015 Grafana Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright 2014-2018 Grafana Labs
|
||||
// Released under the Apache 2.0 license
|
||||
|
||||
// Modification notice:
|
||||
// 1. All field names were changed from toml and snake case to yaml and kebab case,
|
||||
// matching the Oragono project conventions
|
||||
// 2. Four fields were added:
|
||||
// 2.1 `Enabled`
|
||||
// 2.2 `Autocreate`
|
||||
// 2.3 `Timeout`
|
||||
// 2.4 `RequireGroups`
|
||||
|
||||
// XXX: none of AttributeMap does anything in oragono, except MemberOf,
|
||||
// which can be used to retrieve group memberships
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ServerConfig struct {
|
||||
Enabled bool
|
||||
Autocreate bool
|
||||
|
||||
Host string
|
||||
Port int
|
||||
Timeout time.Duration
|
||||
UseSSL bool `yaml:"use-ssl"`
|
||||
StartTLS bool `yaml:"start-tls"`
|
||||
SkipVerifySSL bool `yaml:"ssl-skip-verify"`
|
||||
RootCACert string `yaml:"root-ca-cert"`
|
||||
ClientCert string `yaml:"client-cert"`
|
||||
ClientKey string `yaml:"client-key"`
|
||||
|
||||
BindDN string `yaml:"bind-dn"`
|
||||
BindPassword string `yaml:"bind-password"`
|
||||
SearchFilter string `yaml:"search-filter"`
|
||||
SearchBaseDNs []string `yaml:"search-base-dns"`
|
||||
|
||||
// user validation: require them to be in any one of these groups
|
||||
RequireGroups []string `yaml:"require-groups"`
|
||||
|
||||
// two ways of testing group membership:
|
||||
// either by searching for groups that match the user's DN
|
||||
// and testing their names:
|
||||
GroupSearchFilter string `yaml:"group-search-filter"`
|
||||
GroupSearchFilterUserAttribute string `yaml:"group-search-filter-user-attribute"`
|
||||
GroupSearchBaseDNs []string `yaml:"group-search-base-dns"`
|
||||
|
||||
// or by an attribute on the user's DN, typically named 'memberOf', but customizable:
|
||||
Attr AttributeMap `yaml:"attributes"`
|
||||
}
|
||||
|
||||
// AttributeMap is a struct representation for LDAP "attributes" setting
|
||||
type AttributeMap struct {
|
||||
Username string
|
||||
Name string
|
||||
Surname string
|
||||
Email string
|
||||
MemberOf string `yaml:"member-of"`
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
// Copyright 2014-2018 Grafana Labs
|
||||
// Released under the Apache 2.0 license
|
||||
|
||||
// Modification notice:
|
||||
// 1. `serverConn` was substituted for `Server` as the type of the server object
|
||||
// 2. Debug loglines were altered to work with Oragono's logging system
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidCredentials is returned if username and password do not match
|
||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
||||
|
||||
// ErrCouldNotFindUser is returned when username hasn't been found (not username+password)
|
||||
ErrCouldNotFindUser = errors.New("Can't find user in LDAP")
|
||||
)
|
||||
|
||||
// shouldAdminBind checks if we should use
|
||||
// admin username & password for LDAP bind
|
||||
func (server *serverConn) shouldAdminBind() bool {
|
||||
return server.Config.BindPassword != ""
|
||||
}
|
||||
|
||||
// singleBindDN combines the bind with the username
|
||||
// in order to get the proper path
|
||||
func (server *serverConn) singleBindDN(username string) string {
|
||||
return fmt.Sprintf(server.Config.BindDN, username)
|
||||
}
|
||||
|
||||
// shouldSingleBind checks if we can use "single bind" approach
|
||||
func (server *serverConn) shouldSingleBind() bool {
|
||||
return strings.Contains(server.Config.BindDN, "%s")
|
||||
}
|
||||
|
||||
// Dial dials in the LDAP
|
||||
// TODO: decrease cyclomatic complexity
|
||||
func (server *serverConn) Dial() error {
|
||||
var err error
|
||||
var certPool *x509.CertPool
|
||||
if server.Config.RootCACert != "" {
|
||||
certPool = x509.NewCertPool()
|
||||
for _, caCertFile := range strings.Split(server.Config.RootCACert, " ") {
|
||||
pem, err := ioutil.ReadFile(caCertFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !certPool.AppendCertsFromPEM(pem) {
|
||||
return errors.New("Failed to append CA certificate " + caCertFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
var clientCert tls.Certificate
|
||||
if server.Config.ClientCert != "" && server.Config.ClientKey != "" {
|
||||
clientCert, err = tls.LoadX509KeyPair(server.Config.ClientCert, server.Config.ClientKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, host := range strings.Split(server.Config.Host, " ") {
|
||||
address := fmt.Sprintf("%s:%d", host, server.Config.Port)
|
||||
if server.Config.UseSSL {
|
||||
tlsCfg := &tls.Config{
|
||||
InsecureSkipVerify: server.Config.SkipVerifySSL,
|
||||
ServerName: host,
|
||||
RootCAs: certPool,
|
||||
}
|
||||
if len(clientCert.Certificate) > 0 {
|
||||
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
|
||||
}
|
||||
if server.Config.StartTLS {
|
||||
server.Connection, err = ldap.Dial("tcp", address)
|
||||
if err == nil {
|
||||
if err = server.Connection.StartTLS(tlsCfg); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg)
|
||||
}
|
||||
} else {
|
||||
server.Connection, err = ldap.Dial("tcp", address)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Close closes the LDAP connection
|
||||
// Dial() sets the connection with the server for this Struct. Therefore, we require a
|
||||
// call to Dial() before being able to execute this function.
|
||||
func (server *serverConn) Close() {
|
||||
server.Connection.Close()
|
||||
}
|
||||
|
||||
// userBind binds the user with the LDAP server
|
||||
func (server *serverConn) userBind(path, password string) error {
|
||||
err := server.Connection.Bind(path, password)
|
||||
if err != nil {
|
||||
if ldapErr, ok := err.(*ldap.Error); ok {
|
||||
if ldapErr.ResultCode == 49 {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// users is helper method for the Users()
|
||||
func (server *serverConn) users(logins []string) (
|
||||
[]*ldap.Entry,
|
||||
error,
|
||||
) {
|
||||
var result *ldap.SearchResult
|
||||
var Config = server.Config
|
||||
var err error
|
||||
|
||||
for _, base := range Config.SearchBaseDNs {
|
||||
result, err = server.Connection.Search(
|
||||
server.getSearchRequest(base, logins),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Entries) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result.Entries, nil
|
||||
}
|
||||
|
||||
// getSearchRequest returns LDAP search request for users
|
||||
func (server *serverConn) getSearchRequest(
|
||||
base string,
|
||||
logins []string,
|
||||
) *ldap.SearchRequest {
|
||||
attributes := []string{}
|
||||
|
||||
inputs := server.Config.Attr
|
||||
attributes = appendIfNotEmpty(
|
||||
attributes,
|
||||
inputs.Username,
|
||||
inputs.Surname,
|
||||
inputs.Email,
|
||||
inputs.Name,
|
||||
inputs.MemberOf,
|
||||
|
||||
// In case for the POSIX LDAP schema server
|
||||
server.Config.GroupSearchFilterUserAttribute,
|
||||
)
|
||||
|
||||
search := ""
|
||||
for _, login := range logins {
|
||||
query := strings.Replace(
|
||||
server.Config.SearchFilter,
|
||||
"%s", ldap.EscapeFilter(login),
|
||||
-1,
|
||||
)
|
||||
|
||||
search = search + query
|
||||
}
|
||||
|
||||
filter := fmt.Sprintf("(|%s)", search)
|
||||
|
||||
return &ldap.SearchRequest{
|
||||
BaseDN: base,
|
||||
Scope: ldap.ScopeWholeSubtree,
|
||||
DerefAliases: ldap.NeverDerefAliases,
|
||||
Attributes: attributes,
|
||||
Filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
// requestMemberOf use this function when POSIX LDAP
|
||||
// schema does not support memberOf, so it manually search the groups
|
||||
func (server *serverConn) requestMemberOf(entry *ldap.Entry) ([]string, error) {
|
||||
var memberOf []string
|
||||
var config = server.Config
|
||||
|
||||
for _, groupSearchBase := range config.GroupSearchBaseDNs {
|
||||
var filterReplace string
|
||||
if config.GroupSearchFilterUserAttribute == "" {
|
||||
filterReplace = getAttribute(config.Attr.Username, entry)
|
||||
} else {
|
||||
filterReplace = getAttribute(
|
||||
config.GroupSearchFilterUserAttribute,
|
||||
entry,
|
||||
)
|
||||
}
|
||||
|
||||
filter := strings.Replace(
|
||||
config.GroupSearchFilter, "%s",
|
||||
ldap.EscapeFilter(filterReplace),
|
||||
-1,
|
||||
)
|
||||
|
||||
server.logger.Debug("ldap", "Searching for groups with filter", filter)
|
||||
|
||||
// support old way of reading settings
|
||||
groupIDAttribute := config.Attr.MemberOf
|
||||
// but prefer dn attribute if default settings are used
|
||||
if groupIDAttribute == "" || groupIDAttribute == "memberOf" {
|
||||
groupIDAttribute = "dn"
|
||||
}
|
||||
|
||||
groupSearchReq := ldap.SearchRequest{
|
||||
BaseDN: groupSearchBase,
|
||||
Scope: ldap.ScopeWholeSubtree,
|
||||
DerefAliases: ldap.NeverDerefAliases,
|
||||
Attributes: []string{groupIDAttribute},
|
||||
Filter: filter,
|
||||
}
|
||||
|
||||
groupSearchResult, err := server.Connection.Search(&groupSearchReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(groupSearchResult.Entries) > 0 {
|
||||
for _, group := range groupSearchResult.Entries {
|
||||
|
||||
memberOf = append(
|
||||
memberOf,
|
||||
getAttribute(groupIDAttribute, group),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return memberOf, nil
|
||||
}
|
||||
|
||||
// getMemberOf finds memberOf property or request it
|
||||
func (server *serverConn) getMemberOf(result *ldap.Entry) (
|
||||
[]string, error,
|
||||
) {
|
||||
if server.Config.GroupSearchFilter == "" {
|
||||
memberOf := getArrayAttribute(server.Config.Attr.MemberOf, result)
|
||||
|
||||
return memberOf, nil
|
||||
}
|
||||
|
||||
memberOf, err := server.requestMemberOf(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memberOf, nil
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright 2014-2018 Grafana Labs
|
||||
// Released under the Apache 2.0 license
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
func isMemberOf(memberOf []string, group string) bool {
|
||||
if group == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, member := range memberOf {
|
||||
if strings.EqualFold(member, group) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getArrayAttribute(name string, entry *ldap.Entry) []string {
|
||||
if strings.ToLower(name) == "dn" {
|
||||
return []string{entry.DN}
|
||||
}
|
||||
|
||||
for _, attr := range entry.Attributes {
|
||||
if attr.Name == name && len(attr.Values) > 0 {
|
||||
return attr.Values
|
||||
}
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func getAttribute(name string, entry *ldap.Entry) string {
|
||||
if strings.ToLower(name) == "dn" {
|
||||
return entry.DN
|
||||
}
|
||||
|
||||
for _, attr := range entry.Attributes {
|
||||
if attr.Name == name {
|
||||
if len(attr.Values) > 0 {
|
||||
return attr.Values[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func appendIfNotEmpty(slice []string, values ...string) []string {
|
||||
for _, v := range values {
|
||||
if v != "" {
|
||||
slice = append(slice, v)
|
||||
}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
// Copyright (c) 2020 Matt Ouille
|
||||
// Copyright (c) 2020 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
// Portions of this code copyright Grafana Labs and contributors
|
||||
// and released under the Apache 2.0 license
|
||||
|
||||
// Copying Grafana's original comment on the different cases for LDAP:
|
||||
// There are several cases -
|
||||
// 1. "admin" user
|
||||
// Bind the "admin" user (defined in Grafana config file) which has the search privileges
|
||||
// in LDAP server, then we search the targeted user through that bind, then the second
|
||||
// perform the bind via passed login/password.
|
||||
// 2. Single bind
|
||||
// // If all the users meant to be used with Grafana have the ability to search in LDAP server
|
||||
// then we bind with LDAP server with targeted login/password
|
||||
// and then search for the said user in order to retrive all the information about them
|
||||
// 3. Unauthenticated bind
|
||||
// For some LDAP configurations it is allowed to search the
|
||||
// user without login/password binding with LDAP server, in such case
|
||||
// we will perform "unauthenticated bind", then search for the
|
||||
// targeted user and then perform the bind with passed login/password.
|
||||
|
||||
// Note: the only validation we do on users is to check RequiredGroups.
|
||||
// If RequiredGroups is not set and we can do a single bind, we don't
|
||||
// even need to search. So our case 2 is not restricted
|
||||
// to setups where all the users have search privileges: we only need to
|
||||
// be able to do DN resolution via pure string substitution.
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
|
||||
"github.com/oragono/oragono/irc/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotInRequiredGroup = errors.New("User is not a member of any required groups")
|
||||
)
|
||||
|
||||
// equivalent of Grafana's `Server`, but unexported
|
||||
// also, `log` was renamed to `logger`, since the APIs are slightly different
|
||||
// and this way the compiler will catch any unchanged references to Grafana's `Server.log`
|
||||
type serverConn struct {
|
||||
Config *ServerConfig
|
||||
Connection *ldap.Conn
|
||||
logger *logger.Manager
|
||||
}
|
||||
|
||||
func CheckLDAPPassphrase(config ServerConfig, accountName, passphrase string, log *logger.Manager) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Debug("ldap", "failed passphrase check", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
server := serverConn{
|
||||
Config: &config,
|
||||
logger: log,
|
||||
}
|
||||
|
||||
err = server.Dial()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
server.Connection.SetTimeout(config.Timeout)
|
||||
|
||||
passphraseChecked := false
|
||||
|
||||
if server.shouldSingleBind() {
|
||||
log.Debug("ldap", "attempting single bind to", accountName)
|
||||
err = server.userBind(server.singleBindDN(accountName), passphrase)
|
||||
passphraseChecked = (err == nil)
|
||||
} else if server.shouldAdminBind() {
|
||||
log.Debug("ldap", "attempting admin bind to", config.BindDN)
|
||||
err = server.userBind(config.BindDN, config.BindPassword)
|
||||
} else {
|
||||
log.Debug("ldap", "attempting unauthenticated bind")
|
||||
err = server.Connection.UnauthenticatedBind(config.BindDN)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if passphraseChecked && len(config.RequireGroups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
users, err := server.users([]string{accountName})
|
||||
if err != nil {
|
||||
log.Debug("ldap", "failed user lookup")
|
||||
return err
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return ErrCouldNotFindUser
|
||||
}
|
||||
|
||||
user := users[0]
|
||||
|
||||
log.Debug("ldap", "looked up user", user.DN)
|
||||
|
||||
err = server.validateGroupMembership(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !passphraseChecked {
|
||||
log.Debug("ldap", "rebinding", user.DN)
|
||||
err = server.userBind(user.DN, passphrase)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (server *serverConn) validateGroupMembership(user *ldap.Entry) (err error) {
|
||||
if len(server.Config.RequireGroups) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var memberOf []string
|
||||
memberOf, err = server.getMemberOf(user)
|
||||
if err != nil {
|
||||
server.logger.Debug("ldap", "could not retrieve group memberships", err.Error())
|
||||
return
|
||||
}
|
||||
server.logger.Debug("ldap", fmt.Sprintf("found group memberships: %v", memberOf))
|
||||
foundGroup := false
|
||||
for _, inGroup := range memberOf {
|
||||
for _, acceptableGroup := range server.Config.RequireGroups {
|
||||
if inGroup == acceptableGroup {
|
||||
foundGroup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundGroup {
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundGroup {
|
||||
return nil
|
||||
} else {
|
||||
return ErrUserNotInRequiredGroup
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
@@ -112,12 +111,10 @@ func (nl *NetListener) serve() {
|
||||
// WSListener is a listener for IRC-over-websockets (initially HTTP, then upgraded to a
|
||||
// different application protocol that provides a message-based API, possibly with TLS)
|
||||
type WSListener struct {
|
||||
sync.Mutex // tier 1
|
||||
listener *utils.ReloadableListener
|
||||
httpServer *http.Server
|
||||
server *Server
|
||||
addr string
|
||||
config utils.ListenerConfig
|
||||
}
|
||||
|
||||
func NewWSListener(server *Server, addr string, listener *utils.ReloadableListener, config utils.ListenerConfig) (result *WSListener, err error) {
|
||||
@@ -125,7 +122,6 @@ func NewWSListener(server *Server, addr string, listener *utils.ReloadableListen
|
||||
listener: listener,
|
||||
server: server,
|
||||
addr: addr,
|
||||
config: config,
|
||||
}
|
||||
result.httpServer = &http.Server{
|
||||
Handler: http.HandlerFunc(result.handle),
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ func ApplyUserModeChanges(client *Client, changes modes.ModeChanges, force bool,
|
||||
|
||||
for _, change := range changes {
|
||||
switch change.Mode {
|
||||
case modes.Bot, modes.Invisible, modes.WallOps, modes.UserRoleplaying, modes.Operator, modes.LocalOperator, modes.RegisteredOnly:
|
||||
case modes.Bot, modes.Invisible, modes.WallOps, modes.UserRoleplaying, modes.Operator, modes.LocalOperator, modes.RegisteredOnly, modes.UserNoCTCP:
|
||||
switch change.Op {
|
||||
case modes.Add:
|
||||
if (change.Mode == modes.Operator || change.Mode == modes.LocalOperator) && !(force && oper != nil) {
|
||||
|
||||
@@ -105,6 +105,7 @@ const (
|
||||
RegisteredOnly Mode = 'R'
|
||||
ServerNotice Mode = 's'
|
||||
TLS Mode = 'Z'
|
||||
UserNoCTCP Mode = 'T'
|
||||
UserRoleplaying Mode = 'E'
|
||||
WallOps Mode = 'w'
|
||||
)
|
||||
|
||||
+38
-30
@@ -12,25 +12,24 @@ import (
|
||||
// MonitorManager keeps track of who's monitoring which nicks.
|
||||
type MonitorManager struct {
|
||||
sync.RWMutex // tier 2
|
||||
// client -> nicks it's watching
|
||||
watching map[*Client]map[string]bool
|
||||
// nick -> clients watching it
|
||||
watchedby map[string]map[*Client]bool
|
||||
// (all nicks must be normalized externally by casefolding)
|
||||
// client -> (casefolded nick it's watching -> uncasefolded nick)
|
||||
watching map[*Session]map[string]string
|
||||
// casefolded nick -> clients watching it
|
||||
watchedby map[string]map[*Session]empty
|
||||
}
|
||||
|
||||
func (mm *MonitorManager) Initialize() {
|
||||
mm.watching = make(map[*Client]map[string]bool)
|
||||
mm.watchedby = make(map[string]map[*Client]bool)
|
||||
mm.watching = make(map[*Session]map[string]string)
|
||||
mm.watchedby = make(map[string]map[*Session]empty)
|
||||
}
|
||||
|
||||
// AlertAbout alerts everyone monitoring `client`'s nick that `client` is now {on,off}line.
|
||||
func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool) {
|
||||
var watchers []*Client
|
||||
var watchers []*Session
|
||||
// safely copy the list of clients watching our nick
|
||||
manager.RLock()
|
||||
for client := range manager.watchedby[cfnick] {
|
||||
watchers = append(watchers, client)
|
||||
for session := range manager.watchedby[cfnick] {
|
||||
watchers = append(watchers, session)
|
||||
}
|
||||
manager.RUnlock()
|
||||
|
||||
@@ -39,58 +38,67 @@ func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool) {
|
||||
command = RPL_MONONLINE
|
||||
}
|
||||
|
||||
for _, mClient := range watchers {
|
||||
mClient.Send(nil, mClient.server.name, command, mClient.Nick(), nick)
|
||||
for _, session := range watchers {
|
||||
session.Send(nil, session.client.server.name, command, session.client.Nick(), nick)
|
||||
}
|
||||
}
|
||||
|
||||
// Add registers `client` to receive notifications about `nick`.
|
||||
func (manager *MonitorManager) Add(client *Client, nick string, limit int) error {
|
||||
func (manager *MonitorManager) Add(session *Session, nick string, limit int) error {
|
||||
cfnick, err := CasefoldName(nick)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.Lock()
|
||||
defer manager.Unlock()
|
||||
|
||||
if manager.watching[client] == nil {
|
||||
manager.watching[client] = make(map[string]bool)
|
||||
if manager.watching[session] == nil {
|
||||
manager.watching[session] = make(map[string]string)
|
||||
}
|
||||
if manager.watchedby[nick] == nil {
|
||||
manager.watchedby[nick] = make(map[*Client]bool)
|
||||
if manager.watchedby[cfnick] == nil {
|
||||
manager.watchedby[cfnick] = make(map[*Session]empty)
|
||||
}
|
||||
|
||||
if len(manager.watching[client]) >= limit {
|
||||
if len(manager.watching[session]) >= limit {
|
||||
return errMonitorLimitExceeded
|
||||
}
|
||||
|
||||
manager.watching[client][nick] = true
|
||||
manager.watchedby[nick][client] = true
|
||||
manager.watching[session][cfnick] = nick
|
||||
manager.watchedby[cfnick][session] = empty{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove unregisters `client` from receiving notifications about `nick`.
|
||||
func (manager *MonitorManager) Remove(client *Client, nick string) error {
|
||||
func (manager *MonitorManager) Remove(session *Session, nick string) (err error) {
|
||||
cfnick, err := CasefoldName(nick)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
manager.Lock()
|
||||
defer manager.Unlock()
|
||||
// deleting from nil maps is fine
|
||||
delete(manager.watching[client], nick)
|
||||
delete(manager.watchedby[nick], client)
|
||||
delete(manager.watching[session], cfnick)
|
||||
delete(manager.watchedby[cfnick], session)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAll unregisters `client` from receiving notifications about *all* nicks.
|
||||
func (manager *MonitorManager) RemoveAll(client *Client) {
|
||||
func (manager *MonitorManager) RemoveAll(session *Session) {
|
||||
manager.Lock()
|
||||
defer manager.Unlock()
|
||||
|
||||
for nick := range manager.watching[client] {
|
||||
delete(manager.watchedby[nick], client)
|
||||
for cfnick := range manager.watching[session] {
|
||||
delete(manager.watchedby[cfnick], session)
|
||||
}
|
||||
delete(manager.watching, client)
|
||||
delete(manager.watching, session)
|
||||
}
|
||||
|
||||
// List lists all nicks that `client` is registered to receive notifications about.
|
||||
func (manager *MonitorManager) List(client *Client) (nicks []string) {
|
||||
func (manager *MonitorManager) List(session *Session) (nicks []string) {
|
||||
manager.RLock()
|
||||
defer manager.RUnlock()
|
||||
for nick := range manager.watching[client] {
|
||||
for _, nick := range manager.watching[session] {
|
||||
nicks = append(nicks, nick)
|
||||
}
|
||||
return nicks
|
||||
|
||||
+9
-13
@@ -23,16 +23,13 @@ var (
|
||||
"MemoServ", "BotServ", "OperServ",
|
||||
}
|
||||
|
||||
restrictedCasefoldedNicks = make(map[string]bool)
|
||||
restrictedSkeletons = make(map[string]bool)
|
||||
restrictedCasefoldedNicks = make(utils.StringSet)
|
||||
restrictedSkeletons = make(utils.StringSet)
|
||||
)
|
||||
|
||||
func performNickChange(server *Server, client *Client, target *Client, session *Session, nickname string, rb *ResponseBuffer) error {
|
||||
currentNick := client.Nick()
|
||||
details := target.Details()
|
||||
if details.nick == nickname {
|
||||
return nil
|
||||
}
|
||||
hadNick := details.nick != "*"
|
||||
origNickMask := details.nickMask
|
||||
|
||||
@@ -91,13 +88,11 @@ func performNickChange(server *Server, client *Client, target *Client, session *
|
||||
channel.AddHistoryItem(histItem, details.account)
|
||||
}
|
||||
|
||||
if target.Registered() {
|
||||
newCfnick := target.NickCasefolded()
|
||||
if newCfnick != details.nickCasefolded {
|
||||
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false)
|
||||
client.server.monitorManager.AlertAbout(assignedNickname, newCfnick, true)
|
||||
}
|
||||
} // else: these will be deferred to the end of registration (see #572)
|
||||
newCfnick := target.NickCasefolded()
|
||||
if newCfnick != details.nickCasefolded {
|
||||
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false)
|
||||
client.server.monitorManager.AlertAbout(assignedNickname, newCfnick, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -134,7 +129,8 @@ func fixupNickEqualsAccount(client *Client, rb *ResponseBuffer, config *Config)
|
||||
if !client.registered {
|
||||
return true
|
||||
}
|
||||
if performNickChange(client.server, client, client, rb.session, client.AccountName(), rb) != nil {
|
||||
err := performNickChange(client.server, client, client, rb.session, client.AccountName(), rb)
|
||||
if err != nil && err != errNoop {
|
||||
client.server.accounts.Logout(client)
|
||||
nsNotice(rb, client.t("A client is already using that account; try logging out and logging back in with SASL"))
|
||||
return false
|
||||
|
||||
+60
-12
@@ -244,7 +244,6 @@ SET modifies your account settings. The following settings are available:`,
|
||||
'enforce' lets you specify a custom enforcement mechanism for your registered
|
||||
nicknames. Your options are:
|
||||
1. 'none' [no enforcement, overriding the server default]
|
||||
or else they will be renamed]
|
||||
2. 'strict' [you must already be authenticated to use the nick]
|
||||
3. 'default' [use the server default]`,
|
||||
|
||||
@@ -317,6 +316,24 @@ example with $bCERT ADD <account> <fingerprint>$b.`,
|
||||
enabled: servCmdRequiresAuthEnabled,
|
||||
minParams: 1,
|
||||
},
|
||||
"suspend": {
|
||||
handler: nsSuspendHandler,
|
||||
help: `Syntax: $bSUSPEND <nickname>$b
|
||||
|
||||
SUSPEND disables an account and disconnects the associated clients.`,
|
||||
helpShort: `$bSUSPEND$b disables an account and disconnects the clients`,
|
||||
minParams: 1,
|
||||
capabs: []string{"accreg"},
|
||||
},
|
||||
"unsuspend": {
|
||||
handler: nsUnsuspendHandler,
|
||||
help: `Syntax: $bUNSUSPEND <nickname>$b
|
||||
|
||||
UNSUSPEND reverses a previous SUSPEND, restoring access to the account.`,
|
||||
helpShort: `$bUNSUSPEND$b restores access to a suspended account`,
|
||||
minParams: 1,
|
||||
capabs: []string{"accreg"},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -424,8 +441,8 @@ func displaySetting(settingName string, settings AccountSettings, client *Client
|
||||
}
|
||||
case "dm-history":
|
||||
effectiveValue := historyEnabled(config.History.Persistent.DirectMessages, settings.DMHistory)
|
||||
csNotice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
|
||||
csNotice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
|
||||
|
||||
default:
|
||||
nsNotice(rb, client.t("No such setting"))
|
||||
@@ -445,7 +462,7 @@ func nsSetHandler(server *Server, client *Client, command string, params []strin
|
||||
var finalSettings AccountSettings
|
||||
var err error
|
||||
switch strings.ToLower(params[0]) {
|
||||
case "pass":
|
||||
case "pass", "password":
|
||||
nsNotice(rb, client.t("To change a password, use the PASSWD command. For details, /msg NickServ HELP PASSWD"))
|
||||
return
|
||||
case "enforce":
|
||||
@@ -649,14 +666,11 @@ func nsGroupHandler(server *Server, client *Client, command string, params []str
|
||||
}
|
||||
|
||||
func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) {
|
||||
client.stateMutex.Lock()
|
||||
throttled, remainingTime := client.loginThrottle.Touch()
|
||||
client.stateMutex.Unlock()
|
||||
throttled, remainingTime := client.checkLoginThrottle()
|
||||
if throttled {
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return !throttled
|
||||
}
|
||||
|
||||
func nsIdentifyHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
@@ -685,9 +699,6 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
|
||||
|
||||
// try passphrase
|
||||
if passphrase != "" {
|
||||
if !nsLoginThrottleCheck(client, rb) {
|
||||
return
|
||||
}
|
||||
err = server.accounts.AuthenticateByPassphrase(client, username, passphrase)
|
||||
loginSuccessful = (err == nil)
|
||||
}
|
||||
@@ -804,6 +815,14 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
|
||||
}
|
||||
}
|
||||
|
||||
if passphrase != "" {
|
||||
cfPassphrase, err := Casefold(passphrase)
|
||||
if err == nil && cfPassphrase == details.nickCasefolded {
|
||||
nsNotice(rb, client.t("Usage: REGISTER <passphrase> [email]")) // #1179
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !nsLoginThrottleCheck(client, rb) {
|
||||
return
|
||||
}
|
||||
@@ -1070,6 +1089,9 @@ func nsSessionsHandler(server *Server, client *Client, command string, params []
|
||||
} else {
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Session %d:"), i+1))
|
||||
}
|
||||
if session.deviceID != "" {
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Device ID: %s"), session.deviceID))
|
||||
}
|
||||
nsNotice(rb, fmt.Sprintf(client.t("IP address: %s"), session.ip.String()))
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Hostname: %s"), session.hostname))
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Created at: %s"), session.ctime.Format(time.RFC1123)))
|
||||
@@ -1095,6 +1117,8 @@ func nsCertHandler(server *Server, client *Client, command string, params []stri
|
||||
target, certfp = params[0], params[1]
|
||||
} else if len(params) == 1 {
|
||||
certfp = params[0]
|
||||
} else if len(params) == 0 && verb == "add" && rb.session.certfp != "" {
|
||||
certfp = rb.session.certfp // #1059
|
||||
} else {
|
||||
nsNotice(rb, client.t("Invalid parameters"))
|
||||
return
|
||||
@@ -1171,3 +1195,27 @@ func nsCertHandler(server *Server, client *Client, command string, params []stri
|
||||
nsNotice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
func nsSuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
err := server.accounts.Suspend(params[0])
|
||||
switch err {
|
||||
case nil:
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), params[0]))
|
||||
case errAccountDoesNotExist:
|
||||
nsNotice(rb, client.t("No such account"))
|
||||
default:
|
||||
nsNotice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
func nsUnsuspendHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
err := server.accounts.Unsuspend(params[0])
|
||||
switch err {
|
||||
case nil:
|
||||
nsNotice(rb, fmt.Sprintf(client.t("Successfully un-suspended account %s"), params[0]))
|
||||
case errAccountDoesNotExist:
|
||||
nsNotice(rb, client.t("No such account"))
|
||||
default:
|
||||
nsNotice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -86,6 +86,7 @@ const (
|
||||
RPL_VERSION = "351"
|
||||
RPL_WHOREPLY = "352"
|
||||
RPL_NAMREPLY = "353"
|
||||
RPL_WHOSPCRPL = "354"
|
||||
RPL_LINKS = "364"
|
||||
RPL_ENDOFLINKS = "365"
|
||||
RPL_ENDOFNAMES = "366"
|
||||
@@ -167,8 +168,6 @@ const (
|
||||
ERR_CANNOTSENDRP = "573"
|
||||
RPL_WHOISSECURE = "671"
|
||||
RPL_YOURLANGUAGESARE = "687"
|
||||
ERR_CHANNAMEINUSE = "692"
|
||||
ERR_CANNOTRENAME = "693"
|
||||
ERR_INVALIDMODEPARAM = "696"
|
||||
ERR_LISTMODEALREADYSET = "697"
|
||||
ERR_LISTMODENOTSET = "698"
|
||||
|
||||
+19
-4
@@ -25,7 +25,7 @@ const (
|
||||
type ResponseBuffer struct {
|
||||
Label string // label if this is a labeled response batch
|
||||
batchID string // ID of the labeled response batch, if one has been initiated
|
||||
batchType string // type of the labeled response batch (possibly `history` or `chathistory`)
|
||||
batchType string // type of the labeled response batch (currently either `labeled-response` or `chathistory`)
|
||||
|
||||
// stack of batch IDs of nested batches, which are handled separately
|
||||
// from the underlying labeled-response batch. starting a new nested batch
|
||||
@@ -143,6 +143,23 @@ func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAcc
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *ResponseBuffer) addEchoMessage(tags map[string]string, nickMask, accountName, command, target string, message utils.SplitMessage) {
|
||||
if rb.session.capabilities.Has(caps.EchoMessage) {
|
||||
hasTagsCap := rb.session.capabilities.Has(caps.MessageTags)
|
||||
if command == "TAGMSG" {
|
||||
if hasTagsCap {
|
||||
rb.AddFromClient(message.Time, message.Msgid, nickMask, accountName, tags, command, target)
|
||||
}
|
||||
} else {
|
||||
tagsToSend := tags
|
||||
if !hasTagsCap {
|
||||
tagsToSend = nil
|
||||
}
|
||||
rb.AddSplitMessageFromClient(nickMask, accountName, tagsToSend, command, target, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
|
||||
if rb.batchID != "" {
|
||||
// batch already initialized
|
||||
@@ -200,9 +217,7 @@ func (rb *ResponseBuffer) EndNestedBatch(batchID string) {
|
||||
// supported by the client (`history`, `chathistory`, or no batch, in descending order).
|
||||
func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID string) {
|
||||
var batchType string
|
||||
if rb.session.capabilities.Has(caps.EventPlayback) {
|
||||
batchType = "history"
|
||||
} else if rb.session.capabilities.Has(caps.Batch) {
|
||||
if rb.session.capabilities.Has(caps.Batch) {
|
||||
batchType = "chathistory"
|
||||
}
|
||||
if batchType != "" {
|
||||
|
||||
+2
-2
@@ -110,9 +110,9 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
|
||||
for _, session := range user.Sessions() {
|
||||
session.sendSplitMsgFromClientInternal(false, source, "", nil, "PRIVMSG", tnick, splitMessage)
|
||||
}
|
||||
if user.Away() {
|
||||
if away, awayMessage := user.Away(); away {
|
||||
//TODO(dan): possibly implement cooldown of away notifications to users
|
||||
rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, user.AwayMessage())
|
||||
rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, awayMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-97
@@ -6,7 +6,6 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -21,6 +20,7 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/connection_limits"
|
||||
"github.com/oragono/oragono/irc/history"
|
||||
@@ -41,7 +41,7 @@ var (
|
||||
|
||||
// whitelist of caps to serve on the STS-only listener. In particular,
|
||||
// never advertise SASL, to discourage people from sending their passwords:
|
||||
stsOnlyCaps = caps.NewSet(caps.STS, caps.MessageTags, caps.ServerTime, caps.LabeledResponse, caps.Nope)
|
||||
stsOnlyCaps = caps.NewSet(caps.STS, caps.MessageTags, caps.ServerTime, caps.Batch, caps.LabeledResponse, caps.EchoMessage, caps.Nope)
|
||||
|
||||
// we only have standard channels for now. TODO: any updates to this
|
||||
// will also need to be reflected in CasefoldChannel
|
||||
@@ -80,6 +80,7 @@ type Server struct {
|
||||
whoWas WhoWasList
|
||||
stats Stats
|
||||
semaphores ServerSemaphores
|
||||
defcon uint32
|
||||
}
|
||||
|
||||
// NewServer returns a new Oragono server.
|
||||
@@ -91,6 +92,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
||||
logger: logger,
|
||||
rehashSignal: make(chan os.Signal, 1),
|
||||
signals: make(chan os.Signal, len(ServerExitSignals)),
|
||||
defcon: 5,
|
||||
}
|
||||
|
||||
server.clients.Initialize()
|
||||
@@ -116,6 +118,9 @@ func (server *Server) Shutdown() {
|
||||
//TODO(dan): Make sure we disallow new nicks
|
||||
for _, client := range server.clients.AllClients() {
|
||||
client.Notice("Server is shutting down")
|
||||
if client.AlwaysOn() {
|
||||
client.Store(IncludeLastSeen)
|
||||
}
|
||||
}
|
||||
|
||||
if err := server.store.Close(); err != nil {
|
||||
@@ -146,6 +151,12 @@ func (server *Server) Run() {
|
||||
}
|
||||
|
||||
func (server *Server) checkBans(ipaddr net.IP) (banned bool, message string) {
|
||||
if server.Defcon() == 1 {
|
||||
if !(ipaddr.IsLoopback() || utils.IPInNets(ipaddr, server.Config().Server.secureNets)) {
|
||||
return true, "New connections to this server are temporarily restricted"
|
||||
}
|
||||
}
|
||||
|
||||
// check DLINEs
|
||||
isBanned, info := server.dlines.CheckIP(ipaddr)
|
||||
if isBanned {
|
||||
@@ -211,13 +222,14 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
||||
}
|
||||
|
||||
if c.isSTSOnly {
|
||||
server.playRegistrationBurst(session)
|
||||
server.playSTSBurst(session)
|
||||
return true
|
||||
}
|
||||
|
||||
// client MUST send PASS if necessary, or authenticate with SASL if necessary,
|
||||
// before completing the other registration commands
|
||||
authOutcome := c.isAuthorized(server.Config(), session)
|
||||
config := server.Config()
|
||||
authOutcome := c.isAuthorized(server, config, session)
|
||||
var quitMessage string
|
||||
switch authOutcome {
|
||||
case authFailPass:
|
||||
@@ -267,21 +279,30 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
||||
// Apply default user modes (without updating the invisible counter)
|
||||
// The number of invisible users will be updated by server.stats.Register
|
||||
// if we're using default user mode +i.
|
||||
for _, defaultMode := range server.Config().Accounts.defaultUserModes {
|
||||
for _, defaultMode := range config.Accounts.defaultUserModes {
|
||||
c.SetMode(defaultMode, true)
|
||||
}
|
||||
|
||||
// registration has succeeded:
|
||||
c.SetRegistered()
|
||||
|
||||
// count new user in statistics
|
||||
server.stats.Register(c.HasMode(modes.Invisible))
|
||||
server.monitorManager.AlertAbout(c.Nick(), c.NickCasefolded(), true)
|
||||
|
||||
server.playRegistrationBurst(session)
|
||||
return false
|
||||
}
|
||||
|
||||
func (server *Server) playSTSBurst(session *Session) {
|
||||
nick := utils.SafeErrorParam(session.client.preregNick)
|
||||
session.Send(nil, server.name, RPL_WELCOME, nick, fmt.Sprintf("Welcome to the Internet Relay Network %s", nick))
|
||||
session.Send(nil, server.name, RPL_YOURHOST, nick, fmt.Sprintf("Your host is %[1]s, running version %[2]s", server.name, "oragono"))
|
||||
session.Send(nil, server.name, RPL_CREATED, nick, fmt.Sprintf("This server was created %s", time.Time{}.Format(time.RFC1123)))
|
||||
session.Send(nil, server.name, RPL_MYINFO, nick, server.name, "oragono", "o", "o", "o")
|
||||
session.Send(nil, server.name, RPL_ISUPPORT, nick, "CASEMAPPING=ascii", "are supported by this server")
|
||||
session.Send(nil, server.name, ERR_NOMOTD, nick, "MOTD is unavailable")
|
||||
for _, line := range server.Config().Server.STS.bannerLines {
|
||||
session.Send(nil, server.name, "NOTICE", nick, line)
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) playRegistrationBurst(session *Session) {
|
||||
c := session.client
|
||||
// continue registration
|
||||
@@ -297,13 +318,6 @@ func (server *Server) playRegistrationBurst(session *Session) {
|
||||
session.Send(nil, server.name, RPL_CREATED, d.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123)))
|
||||
session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, rplMyInfo1, rplMyInfo2, rplMyInfo3)
|
||||
|
||||
if c.isSTSOnly {
|
||||
for _, line := range server.Config().Server.STS.bannerLines {
|
||||
c.Notice(line)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
rb := NewResponseBuffer(session)
|
||||
server.RplISupport(c, rb)
|
||||
server.Lusers(c, rb)
|
||||
@@ -418,40 +432,11 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
||||
}
|
||||
}
|
||||
rb.Add(nil, client.server.name, RPL_WHOISIDLE, cnick, tnick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), client.t("seconds idle, signon time"))
|
||||
if target.Away() {
|
||||
rb.Add(nil, client.server.name, RPL_AWAY, cnick, tnick, target.AwayMessage())
|
||||
if away, awayMessage := target.Away(); away {
|
||||
rb.Add(nil, client.server.name, RPL_AWAY, cnick, tnick, awayMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// rplWhoReply returns the WHO reply between one user and another channel/user.
|
||||
// <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ]
|
||||
// :<hopcount> <real name>
|
||||
func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *ResponseBuffer) {
|
||||
channelName := "*"
|
||||
flags := ""
|
||||
|
||||
if target.Away() {
|
||||
flags = "G"
|
||||
} else {
|
||||
flags = "H"
|
||||
}
|
||||
if target.HasMode(modes.Operator) {
|
||||
flags += "*"
|
||||
}
|
||||
|
||||
if channel != nil {
|
||||
// TODO is this right?
|
||||
flags += channel.ClientPrefixes(target, rb.session.capabilities.Has(caps.MultiPrefix))
|
||||
channelName = channel.name
|
||||
}
|
||||
if target.HasMode(modes.Bot) {
|
||||
flags += "B"
|
||||
}
|
||||
details := target.Details()
|
||||
// hardcode a hopcount of 0 for now
|
||||
rb.Add(nil, client.server.name, RPL_WHOREPLY, client.Nick(), channelName, details.username, details.hostname, client.server.name, details.nick, flags, "0 "+details.realname)
|
||||
}
|
||||
|
||||
// rehash reloads the config and applies the changes from the config file.
|
||||
func (server *Server) rehash() error {
|
||||
server.logger.Info("server", "Attempting rehash")
|
||||
@@ -487,6 +472,7 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
||||
server.name = config.Server.Name
|
||||
server.nameCasefolded = config.Server.nameCasefolded
|
||||
globalCasemappingSetting = config.Server.Casemapping
|
||||
globalUtf8EnforcementSetting = config.Server.EnforceUtf8
|
||||
} else {
|
||||
// enforce configs that can't be changed after launch:
|
||||
if server.name != config.Server.Name {
|
||||
@@ -495,6 +481,8 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
||||
return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
|
||||
} else if globalCasemappingSetting != config.Server.Casemapping {
|
||||
return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
|
||||
} else if globalUtf8EnforcementSetting != config.Server.EnforceUtf8 {
|
||||
return fmt.Errorf("UTF-8 enforcement cannot be changed after launching the server, rehash aborted")
|
||||
} else if oldConfig.Accounts.Multiclient.AlwaysOn != config.Accounts.Multiclient.AlwaysOn {
|
||||
return fmt.Errorf("Default always-on setting cannot be changed after launching the server, rehash aborted")
|
||||
} else if oldConfig.Server.Relaying.Enabled != config.Server.Relaying.Enabled {
|
||||
@@ -538,7 +526,7 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
||||
server.channels.loadRegisteredChannels(config)
|
||||
}
|
||||
// resize history buffers as needed
|
||||
if oldConfig.History != config.History {
|
||||
if config.historyChangedFrom(oldConfig) {
|
||||
for _, channel := range server.channels.Channels() {
|
||||
channel.resizeHistory(config)
|
||||
}
|
||||
@@ -665,35 +653,6 @@ func (server *Server) setupPprofListener(config *Config) {
|
||||
}
|
||||
}
|
||||
|
||||
func (config *Config) loadMOTD() (err error) {
|
||||
if config.Server.MOTD != "" {
|
||||
file, err := os.Open(config.Server.MOTD)
|
||||
if err == nil {
|
||||
defer file.Close()
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
|
||||
if config.Server.MOTDFormatting {
|
||||
line = ircfmt.Unescape(line)
|
||||
}
|
||||
|
||||
// "- " is the required prefix for MOTD, we just add it here to make
|
||||
// bursting it out to clients easier
|
||||
line = fmt.Sprintf("- %s", line)
|
||||
|
||||
config.Server.motdLines = append(config.Server.motdLines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (server *Server) loadDatastore(config *Config) error {
|
||||
// open the datastore and load server state for which it (rather than config)
|
||||
// is the source of truth
|
||||
@@ -968,26 +927,6 @@ func (matcher *elistMatcher) Matches(channel *Channel) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// RplList returns the RPL_LIST numeric for the given channel.
|
||||
func (target *Client) RplList(channel *Channel, rb *ResponseBuffer) {
|
||||
// get the correct number of channel members
|
||||
var memberCount int
|
||||
if target.HasMode(modes.Operator) || channel.hasClient(target) {
|
||||
memberCount = len(channel.Members())
|
||||
} else {
|
||||
for _, member := range channel.Members() {
|
||||
if !member.HasMode(modes.Invisible) {
|
||||
memberCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #704: some channels are kept around even with no members
|
||||
if memberCount != 0 {
|
||||
rb.Add(nil, target.server.name, RPL_LIST, target.nick, channel.name, strconv.Itoa(memberCount), channel.topic)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
infoString1 = strings.Split(` ▄▄▄ ▄▄▄· ▄▄ • ▐ ▄
|
||||
▪ ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪ •█▌▐█▪
|
||||
|
||||
+2
-2
@@ -350,11 +350,11 @@ func initializeServices() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
restrictedCasefoldedNicks[cfName] = true
|
||||
restrictedCasefoldedNicks.Add(cfName)
|
||||
skeleton, err := Skeleton(restrictedNickname)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
restrictedSkeletons[skeleton] = true
|
||||
restrictedSkeletons.Add(skeleton)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-11
@@ -7,7 +7,6 @@ package irc
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
@@ -59,27 +58,24 @@ func (socket *Socket) Close() {
|
||||
|
||||
// Read returns a single IRC line from a Socket.
|
||||
func (socket *Socket) Read() (string, error) {
|
||||
// immediately fail if Close() has been called, even if there's
|
||||
// still data in a bufio.Reader or websocket buffer:
|
||||
if socket.IsClosed() {
|
||||
return "", io.EOF
|
||||
}
|
||||
|
||||
lineBytes, err := socket.conn.ReadLine()
|
||||
|
||||
// convert bytes to string
|
||||
line := string(lineBytes)
|
||||
|
||||
// read last message properly (such as ERROR/QUIT/etc), just fail next reads/writes
|
||||
if err == io.EOF {
|
||||
socket.Close()
|
||||
// process last message properly (such as ERROR/QUIT/etc), just fail next reads/writes
|
||||
if line != "" {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF && strings.TrimSpace(line) != "" {
|
||||
// don't do anything
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return line, nil
|
||||
return line, err
|
||||
}
|
||||
|
||||
// Write sends the given string out of Socket. Requirements:
|
||||
|
||||
@@ -60,6 +60,12 @@ const (
|
||||
// this happens-before all IRC connections and all casefolding operations.
|
||||
var globalCasemappingSetting Casemapping = CasemappingPRECIS
|
||||
|
||||
// XXX analogous unsynchronized global variable controlling utf8 validation
|
||||
// if this is off, you get the traditional IRC behavior (relaying any valid RFC1459
|
||||
// octets) and invalid utf8 messages are silently dropped for websocket clients only.
|
||||
// if this is on, invalid utf8 inputs get a FAIL reply.
|
||||
var globalUtf8EnforcementSetting bool
|
||||
|
||||
// Each pass of PRECIS casefolding is a composition of idempotent operations,
|
||||
// but not idempotent itself. Therefore, the spec says "do it four times and hope
|
||||
// it converges" (lolwtf). Golang's PRECIS implementation has a "repeat" option,
|
||||
|
||||
+1
-12
@@ -28,17 +28,6 @@ func (clients ClientSet) Has(client *Client) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
type StringSet map[string]empty
|
||||
|
||||
func (s StringSet) Has(str string) bool {
|
||||
_, ok := s[str]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s StringSet) Add(str string) {
|
||||
s[str] = empty{}
|
||||
}
|
||||
|
||||
// MemberSet is a set of members with modes.
|
||||
type MemberSet map[*Client]*modes.ModeSet
|
||||
|
||||
@@ -69,4 +58,4 @@ func (members MemberSet) AnyHasMode(mode modes.Mode) bool {
|
||||
}
|
||||
|
||||
// ChannelSet is a set of channels.
|
||||
type ChannelSet map[*Channel]bool
|
||||
type ChannelSet map[*Channel]empty
|
||||
|
||||
+4
-4
@@ -4,14 +4,14 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"regexp/syntax"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// yet another glob implementation in Go
|
||||
|
||||
func addRegexp(buf *bytes.Buffer, glob string, submatch bool) (err error) {
|
||||
func addRegexp(buf *strings.Builder, glob string, submatch bool) (err error) {
|
||||
for _, r := range glob {
|
||||
switch r {
|
||||
case '*':
|
||||
@@ -36,7 +36,7 @@ func addRegexp(buf *bytes.Buffer, glob string, submatch bool) (err error) {
|
||||
}
|
||||
|
||||
func CompileGlob(glob string, submatch bool) (result *regexp.Regexp, err error) {
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
buf.WriteByte('^')
|
||||
err = addRegexp(&buf, glob, submatch)
|
||||
if err != nil {
|
||||
@@ -50,7 +50,7 @@ func CompileGlob(glob string, submatch bool) (result *regexp.Regexp, err error)
|
||||
// This is used for channel ban/invite/exception lists. It's applicable to k-lines
|
||||
// but we're not using it there yet.
|
||||
func CompileMasks(masks []string) (result *regexp.Regexp, err error) {
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
buf.WriteString("^(")
|
||||
for i, mask := range masks {
|
||||
err = addRegexp(&buf, mask, false)
|
||||
|
||||
@@ -10,27 +10,25 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type e struct{}
|
||||
|
||||
// Semaphore is a counting semaphore.
|
||||
// A semaphore of capacity 1 can be used as a trylock.
|
||||
type Semaphore (chan e)
|
||||
type Semaphore (chan empty)
|
||||
|
||||
// Initialize initializes a semaphore to a given capacity.
|
||||
func (semaphore *Semaphore) Initialize(capacity int) {
|
||||
*semaphore = make(chan e, capacity)
|
||||
*semaphore = make(chan empty, capacity)
|
||||
}
|
||||
|
||||
// Acquire acquires a semaphore, blocking if necessary.
|
||||
func (semaphore *Semaphore) Acquire() {
|
||||
(*semaphore) <- e{}
|
||||
(*semaphore) <- empty{}
|
||||
}
|
||||
|
||||
// TryAcquire tries to acquire a semaphore, returning whether the acquire was
|
||||
// successful. It never blocks.
|
||||
func (semaphore *Semaphore) TryAcquire() (acquired bool) {
|
||||
select {
|
||||
case (*semaphore) <- e{}:
|
||||
case (*semaphore) <- empty{}:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -47,7 +45,7 @@ func (semaphore *Semaphore) AcquireWithTimeout(timeout time.Duration) (acquired
|
||||
|
||||
timer := time.NewTimer(timeout)
|
||||
select {
|
||||
case (*semaphore) <- e{}:
|
||||
case (*semaphore) <- empty{}:
|
||||
acquired = true
|
||||
case <-timer.C:
|
||||
acquired = false
|
||||
@@ -61,7 +59,7 @@ func (semaphore *Semaphore) AcquireWithTimeout(timeout time.Duration) (acquired
|
||||
// Note that if the context is already expired, the acquire may succeed anyway.
|
||||
func (semaphore *Semaphore) AcquireWithContext(ctx context.Context) (acquired bool) {
|
||||
select {
|
||||
case (*semaphore) <- e{}:
|
||||
case (*semaphore) <- empty{}:
|
||||
acquired = true
|
||||
case <-ctx.Done():
|
||||
acquired = false
|
||||
|
||||
+1
-2
@@ -4,7 +4,6 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -98,7 +97,7 @@ func (sm *SplitMessage) Is512() bool {
|
||||
type TokenLineBuilder struct {
|
||||
lineLen int
|
||||
delim string
|
||||
buf bytes.Buffer
|
||||
buf strings.Builder
|
||||
result []string
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2020 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package utils
|
||||
|
||||
type empty struct{}
|
||||
|
||||
type StringSet map[string]empty
|
||||
|
||||
func (s StringSet) Has(str string) bool {
|
||||
_, ok := s[str]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s StringSet) Add(str string) {
|
||||
s[str] = empty{}
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ import "fmt"
|
||||
|
||||
const (
|
||||
// SemVer is the semantic version of Oragono.
|
||||
SemVer = "2.2.0-unreleased"
|
||||
SemVer = "2.4.0-unreleased"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
+55
-8
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/history"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -53,10 +54,16 @@ func zncWireTimeToTime(str string) (result time.Time) {
|
||||
return time.Unix(seconds, int64(fraction*1000000000)).UTC()
|
||||
}
|
||||
|
||||
func timeToZncWireTime(t time.Time) (result string) {
|
||||
secs := t.Unix()
|
||||
nano := t.UnixNano() - (secs * 1000000000)
|
||||
return fmt.Sprintf("%d.%d", secs, nano)
|
||||
}
|
||||
|
||||
type zncPlaybackTimes struct {
|
||||
start time.Time
|
||||
end time.Time
|
||||
targets StringSet // nil for "*" (everything), otherwise the channel names
|
||||
targets utils.StringSet // nil for "*" (everything), otherwise the channel names
|
||||
setAt time.Time
|
||||
}
|
||||
|
||||
@@ -77,19 +84,33 @@ func (z *zncPlaybackTimes) ValidFor(target string) bool {
|
||||
}
|
||||
|
||||
// https://wiki.znc.in/Playback
|
||||
func zncPlaybackHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
if len(params) == 0 {
|
||||
return
|
||||
}
|
||||
switch strings.ToLower(params[0]) {
|
||||
case "play":
|
||||
zncPlaybackPlayHandler(client, command, params, rb)
|
||||
case "list":
|
||||
zncPlaybackListHandler(client, command, params, rb)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVMSG *playback :play <target> [lower_bound] [upper_bound]
|
||||
// e.g., PRIVMSG *playback :play * 1558374442
|
||||
func zncPlaybackHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
func zncPlaybackPlayHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
if len(params) < 2 || len(params) > 4 {
|
||||
return
|
||||
} else if strings.ToLower(params[0]) != "play" {
|
||||
return
|
||||
}
|
||||
targetString := params[1]
|
||||
|
||||
now := time.Now().UTC()
|
||||
var start, end time.Time
|
||||
switch len(params) {
|
||||
case 2:
|
||||
// #1205: this should have the same semantics as `LATEST *`
|
||||
case 3:
|
||||
// #831: this should have the same semantics as `LATEST timestamp=qux`,
|
||||
// or equivalently `BETWEEN timestamp=$now timestamp=qux`, as opposed to
|
||||
@@ -102,7 +123,7 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
|
||||
end = zncWireTimeToTime(params[3])
|
||||
}
|
||||
|
||||
var targets StringSet
|
||||
var targets utils.StringSet
|
||||
var nickTargets []string
|
||||
|
||||
// three cases:
|
||||
@@ -121,12 +142,15 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
|
||||
// 3.3 When the client sends a subsequent redundant JOIN line for those
|
||||
// channels; redundant JOIN is a complete no-op so we won't replay twice
|
||||
|
||||
playPrivmsgs := false
|
||||
if params[1] == "*" {
|
||||
zncPlayPrivmsgs(client, rb, "*", start, end)
|
||||
playPrivmsgs = true // XXX nil `targets` means "every channel"
|
||||
} else {
|
||||
targets = make(StringSet)
|
||||
targets = make(utils.StringSet)
|
||||
for _, targetName := range strings.Split(targetString, ",") {
|
||||
if strings.HasPrefix(targetName, "#") {
|
||||
if targetName == "*self" {
|
||||
playPrivmsgs = true
|
||||
} else if strings.HasPrefix(targetName, "#") {
|
||||
if cfTarget, err := CasefoldChannel(targetName); err == nil {
|
||||
targets.Add(cfTarget)
|
||||
}
|
||||
@@ -138,6 +162,10 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res
|
||||
}
|
||||
}
|
||||
|
||||
if playPrivmsgs {
|
||||
zncPlayPrivmsgs(client, rb, "*", start, end)
|
||||
}
|
||||
|
||||
rb.session.zncPlaybackTimes = &zncPlaybackTimes{
|
||||
start: start,
|
||||
end: end,
|
||||
@@ -169,3 +197,22 @@ func zncPlayPrivmsgs(client *Client, rb *ResponseBuffer, target string, after, b
|
||||
client.replayPrivmsgHistory(rb, items, "", true)
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVMSG *playback :list
|
||||
func zncPlaybackListHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
nick := client.Nick()
|
||||
for _, channel := range client.Channels() {
|
||||
_, sequence, err := client.server.GetHistorySequence(channel, client, "")
|
||||
if err != nil {
|
||||
client.server.logger.Error("internal", "couldn't get history sequence for ZNC list", err.Error())
|
||||
continue
|
||||
}
|
||||
items, _, err := sequence.Between(history.Selector{}, history.Selector{}, 1) // i.e., LATEST * 1
|
||||
if err != nil {
|
||||
client.server.logger.Error("internal", "couldn't query history for ZNC list", err.Error())
|
||||
} else if len(items) != 0 {
|
||||
stamp := timeToZncWireTime(items[0].Message.Time)
|
||||
rb.Add(nil, "*playback!znc@znc.in", "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", channel.Name(), stamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user