Merge remote-tracking branch 'origin/master' into master+relaymsg

This commit is contained in:
Shivaram Lingamneni
2020-09-09 03:55:41 -04:00
328 changed files with 24635 additions and 16388 deletions
+133 -88
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
-23
View File
@@ -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) {
+1 -2
View File
@@ -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
View File
@@ -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)
-170
View File
@@ -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
View File
@@ -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
}
}
+77
View File
@@ -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
}
}
-202
View File
@@ -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.
-62
View File
@@ -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"`
}
-267
View File
@@ -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
}
-60
View File
@@ -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
}
-152
View File
@@ -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
}
}
-4
View File
@@ -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
View File
@@ -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) {
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+6 -8
View File
@@ -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
View File
@@ -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
}
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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))
}
}
}