diff --git a/irc/channel.go b/irc/channel.go index 67c6d332..878d73c6 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -1227,24 +1227,38 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe // CanSpeak returns true if the client can speak on this channel, otherwise it returns false along with the channel mode preventing the client from speaking. func (channel *Channel) CanSpeak(client *Client) (bool, modes.Mode) { channel.stateMutex.RLock() - defer channel.stateMutex.RUnlock() + clientModes, hasClient := channel.members[client] + channel.stateMutex.RUnlock() - _, hasClient := channel.members[client] - if channel.flags.HasMode(modes.NoOutside) && !hasClient { + if !hasClient && channel.flags.HasMode(modes.NoOutside) { + // TODO: enforce regular +b bans on -n channels? return false, modes.NoOutside } - if channel.flags.HasMode(modes.Moderated) && !channel.ClientIsAtLeast(client, modes.Voice) { + if channel.isMuted(client) && clientModes.HighestChannelUserMode() == modes.Mode(0) { + return false, modes.BanMask + } + if channel.flags.HasMode(modes.Moderated) && clientModes.HighestChannelUserMode() == modes.Mode(0) { return false, modes.Moderated } if channel.flags.HasMode(modes.RegisteredOnly) && client.Account() == "" { return false, modes.RegisteredOnly } - if channel.flags.HasMode(modes.RegisteredOnlySpeak) && client.Account() == "" && !channel.ClientIsAtLeast(client, modes.Voice) { + if channel.flags.HasMode(modes.RegisteredOnlySpeak) && client.Account() == "" && + clientModes.HighestChannelUserMode() != modes.Mode(0) { return false, modes.RegisteredOnlySpeak } return true, modes.Mode('?') } +func (channel *Channel) isMuted(client *Client) bool { + muteRe := channel.lists[modes.BanMask].MuteRegexp() + if muteRe == nil { + return false + } + nuh := client.NickMaskString() + return muteRe.MatchString(nuh) && !channel.lists[modes.ExceptMask].MatchMute(nuh) +} + func msgCommandToHistType(command string) (history.ItemType, error) { switch command { case "PRIVMSG": diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 519a3bb5..8aef784e 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -5,10 +5,8 @@ package irc import ( - "regexp" "strings" "sync" - "time" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/modes" @@ -306,134 +304,3 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) { return set } - -// -// usermask to regexp -// - -//TODO(dan): move this over to generally using glob syntax instead? -// kinda more expected in normal ban/etc masks, though regex is useful (probably as an extban?) - -type MaskInfo struct { - TimeCreated time.Time - CreatorNickmask string - CreatorAccount string -} - -// UserMaskSet holds a set of client masks and lets you match hostnames to them. -type UserMaskSet struct { - sync.RWMutex - serialCacheUpdateMutex sync.Mutex - masks map[string]MaskInfo - regexp *regexp.Regexp -} - -func NewUserMaskSet() *UserMaskSet { - return new(UserMaskSet) -} - -// Add adds the given mask to this set. -func (set *UserMaskSet) Add(mask, creatorNickmask, creatorAccount string) (maskAdded string, err error) { - casefoldedMask, err := CanonicalizeMaskWildcard(mask) - if err != nil { - return - } - - set.serialCacheUpdateMutex.Lock() - defer set.serialCacheUpdateMutex.Unlock() - - set.Lock() - if set.masks == nil { - set.masks = make(map[string]MaskInfo) - } - _, present := set.masks[casefoldedMask] - if !present { - maskAdded = casefoldedMask - set.masks[casefoldedMask] = MaskInfo{ - TimeCreated: time.Now().UTC(), - CreatorNickmask: creatorNickmask, - CreatorAccount: creatorAccount, - } - } - set.Unlock() - - if !present { - set.setRegexp() - } - return -} - -// Remove removes the given mask from this set. -func (set *UserMaskSet) Remove(mask string) (maskRemoved string, err error) { - mask, err = CanonicalizeMaskWildcard(mask) - if err != nil { - return - } - - set.serialCacheUpdateMutex.Lock() - defer set.serialCacheUpdateMutex.Unlock() - - set.Lock() - _, removed := set.masks[mask] - if removed { - maskRemoved = mask - delete(set.masks, mask) - } - set.Unlock() - - if removed { - set.setRegexp() - } - return -} - -func (set *UserMaskSet) SetMasks(masks map[string]MaskInfo) { - set.Lock() - set.masks = masks - set.Unlock() - set.setRegexp() -} - -func (set *UserMaskSet) Masks() (result map[string]MaskInfo) { - set.RLock() - defer set.RUnlock() - - result = make(map[string]MaskInfo, len(set.masks)) - for mask, info := range set.masks { - result[mask] = info - } - return -} - -// Match matches the given n!u@h. -func (set *UserMaskSet) Match(userhost string) bool { - set.RLock() - regexp := set.regexp - set.RUnlock() - - if regexp == nil { - return false - } - return regexp.MatchString(userhost) -} - -func (set *UserMaskSet) Length() int { - set.RLock() - defer set.RUnlock() - return len(set.masks) -} - -func (set *UserMaskSet) setRegexp() { - set.RLock() - maskExprs := make([]string, len(set.masks)) - for mask := range set.masks { - maskExprs = append(maskExprs, mask) - } - set.RUnlock() - - re, _ := utils.CompileMasks(maskExprs) - - set.Lock() - set.regexp = re - set.Unlock() -} diff --git a/irc/config.go b/irc/config.go index f169522c..4c5ee631 100644 --- a/irc/config.go +++ b/irc/config.go @@ -1298,6 +1298,7 @@ func (config *Config) generateISupport() (err error) { if config.Extjwt.Default.Enabled() || len(config.Extjwt.Services) != 0 { isupport.Add("EXTJWT", "1") } + isupport.Add("EXTBAN", ",m") isupport.Add("INVEX", "") isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen)) isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes))) diff --git a/irc/usermaskset.go b/irc/usermaskset.go new file mode 100644 index 00000000..e8c97145 --- /dev/null +++ b/irc/usermaskset.go @@ -0,0 +1,167 @@ +// Copyright (c) 2012-2014 Jeremy Latt +// Copyright (c) 2016-2018 Daniel Oaks +// Copyright (c) 2019-2020 Shivaram Lingamneni +// released under the MIT license + +package irc + +import ( + "regexp" + "strings" + "sync" + "sync/atomic" + "time" + "unsafe" + + "github.com/oragono/oragono/irc/utils" +) + +type MaskInfo struct { + TimeCreated time.Time + CreatorNickmask string + CreatorAccount string +} + +// UserMaskSet holds a set of client masks and lets you match hostnames to them. +type UserMaskSet struct { + sync.RWMutex + serialCacheUpdateMutex sync.Mutex + masks map[string]MaskInfo + regexp unsafe.Pointer + muteRegexp unsafe.Pointer +} + +func NewUserMaskSet() *UserMaskSet { + return new(UserMaskSet) +} + +// Add adds the given mask to this set. +func (set *UserMaskSet) Add(mask, creatorNickmask, creatorAccount string) (maskAdded string, err error) { + casefoldedMask, err := CanonicalizeMaskWildcard(mask) + if err != nil { + return + } + + set.serialCacheUpdateMutex.Lock() + defer set.serialCacheUpdateMutex.Unlock() + + set.Lock() + if set.masks == nil { + set.masks = make(map[string]MaskInfo) + } + _, present := set.masks[casefoldedMask] + if !present { + maskAdded = casefoldedMask + set.masks[casefoldedMask] = MaskInfo{ + TimeCreated: time.Now().UTC(), + CreatorNickmask: creatorNickmask, + CreatorAccount: creatorAccount, + } + } + set.Unlock() + + if !present { + set.setRegexp() + } + return +} + +// Remove removes the given mask from this set. +func (set *UserMaskSet) Remove(mask string) (maskRemoved string, err error) { + mask, err = CanonicalizeMaskWildcard(mask) + if err != nil { + return + } + + set.serialCacheUpdateMutex.Lock() + defer set.serialCacheUpdateMutex.Unlock() + + set.Lock() + _, removed := set.masks[mask] + if removed { + maskRemoved = mask + delete(set.masks, mask) + } + set.Unlock() + + if removed { + set.setRegexp() + } + return +} + +func (set *UserMaskSet) SetMasks(masks map[string]MaskInfo) { + set.Lock() + set.masks = masks + set.Unlock() + set.setRegexp() +} + +func (set *UserMaskSet) Masks() (result map[string]MaskInfo) { + set.RLock() + defer set.RUnlock() + + result = make(map[string]MaskInfo, len(set.masks)) + for mask, info := range set.masks { + result[mask] = info + } + return +} + +// Match matches the given n!u@h against the standard (non-ext) bans. +func (set *UserMaskSet) Match(userhost string) bool { + regexp := (*regexp.Regexp)(atomic.LoadPointer(&set.regexp)) + + if regexp == nil { + return false + } + return regexp.MatchString(userhost) +} + +// MatchMute matches the given NUH against the mute extbans. +func (set *UserMaskSet) MatchMute(userhost string) bool { + regexp := set.MuteRegexp() + + if regexp == nil { + return false + } + return regexp.MatchString(userhost) +} + +func (set *UserMaskSet) MuteRegexp() *regexp.Regexp { + return (*regexp.Regexp)(atomic.LoadPointer(&set.muteRegexp)) +} + +func (set *UserMaskSet) Length() int { + set.RLock() + defer set.RUnlock() + return len(set.masks) +} + +func (set *UserMaskSet) setRegexp() { + set.RLock() + maskExprs := make([]string, 0, len(set.masks)) + var muteExprs []string + for mask := range set.masks { + if strings.HasPrefix(mask, "m:") { + muteExprs = append(muteExprs, mask[2:]) + } else { + maskExprs = append(maskExprs, mask) + } + } + set.RUnlock() + + compileMasks := func(masks []string) *regexp.Regexp { + if len(masks) == 0 { + return nil + } + re, _ := utils.CompileMasks(masks) + return re + } + + re := compileMasks(maskExprs) + muteRe := compileMasks(muteExprs) + + atomic.StorePointer(&set.regexp, unsafe.Pointer(re)) + atomic.StorePointer(&set.muteRegexp, unsafe.Pointer(muteRe)) +} diff --git a/irc/usermaskset_test.go b/irc/usermaskset_test.go new file mode 100644 index 00000000..412dd196 --- /dev/null +++ b/irc/usermaskset_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package irc + +import ( + "testing" +) + +func TestUserMaskSet(t *testing.T) { + s := NewUserMaskSet() + + if s.Match("horse!~evan@tor-network.onion") { + t.Errorf("empty set should not match anything") + } + + s.Add("m:horse!*@*", "", "") + if s.Match("horse!~evan@tor-network.onion") { + t.Errorf("mute extbans should not Match(), only MatchMute()") + } + + s.Add("*!~evan@*", "", "") + if !s.Match("horse!~evan@tor-network.onion") { + t.Errorf("expected Match() failed") + } + if s.Match("horse!~horse@tor-network.onion") { + t.Errorf("unexpected Match() succeeded") + } + + if !s.MatchMute("horse!~evan@tor-network.onion") { + t.Errorf("expected MatchMute() failed") + } + if s.MatchMute("evan!~evan@tor-network.onion") { + t.Errorf("unexpected MatchMute() succeeded") + } +}