diff --git a/irc/channel.go b/irc/channel.go index a38ebca3..e619b8bf 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -124,18 +124,12 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) { for _, mode := range chanReg.Modes { channel.flags.SetMode(mode, true) } - for _, mask := range chanReg.Banlist { - channel.lists[modes.BanMask].Add(mask) - } - for _, mask := range chanReg.Exceptlist { - channel.lists[modes.ExceptMask].Add(mask) - } - for _, mask := range chanReg.Invitelist { - channel.lists[modes.InviteMask].Add(mask) - } for account, mode := range chanReg.AccountToUMode { channel.accountToUMode[account] = mode } + channel.lists[modes.BanMask].SetMasks(chanReg.Bans) + channel.lists[modes.InviteMask].SetMasks(chanReg.Invites) + channel.lists[modes.ExceptMask].SetMasks(chanReg.Excepts) } // obtain a consistent snapshot of the channel state that can be persisted to the DB @@ -160,15 +154,9 @@ func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredCh } if includeFlags&IncludeLists != 0 { - for mask := range channel.lists[modes.BanMask].masks { - info.Banlist = append(info.Banlist, mask) - } - for mask := range channel.lists[modes.ExceptMask].masks { - info.Exceptlist = append(info.Exceptlist, mask) - } - for mask := range channel.lists[modes.InviteMask].masks { - info.Invitelist = append(info.Invitelist, mask) - } + info.Bans = channel.lists[modes.BanMask].Masks() + info.Invites = channel.lists[modes.InviteMask].Masks() + info.Excepts = channel.lists[modes.ExceptMask].Masks() info.AccountToUMode = make(map[string]modes.Mode) for account, mode := range channel.accountToUMode { info.AccountToUMode[account] = mode @@ -1097,14 +1085,12 @@ func (channel *Channel) ShowMaskList(client *Client, mode modes.Mode, rb *Respon } nick := client.Nick() - channel.stateMutex.RLock() - // XXX don't acquire any new locks in this section, besides Socket.Write - for mask := range channel.lists[mode].masks { - rb.Add(nil, client.server.name, rpllist, nick, channel.name, mask) + chname := channel.Name() + for mask, info := range channel.lists[mode].Masks() { + rb.Add(nil, client.server.name, rpllist, nick, chname, mask, info.CreatorNickmask, strconv.FormatInt(info.TimeCreated.Unix(), 10)) } - channel.stateMutex.RUnlock() - rb.Add(nil, client.server.name, rplendoflist, nick, channel.name, client.t("End of list")) + rb.Add(nil, client.server.name, rplendoflist, nick, chname, client.t("End of list")) } // Quit removes the given client from the channel diff --git a/irc/channelreg.go b/irc/channelreg.go index 19cae5b8..6f93104f 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -88,12 +88,12 @@ type RegisteredChannel struct { Key string // AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h) AccountToUMode map[string]modes.Mode - // Banlist represents the bans set on the channel. - Banlist []string - // Exceptlist represents the exceptions set on the channel. - Exceptlist []string - // Invitelist represents the invite exceptions set on the channel. - Invitelist []string + // Bans represents the bans set on the channel. + Bans map[string]MaskInfo + // Excepts represents the exceptions set on the channel. + Excepts map[string]MaskInfo + // Invites represents the invite exceptions set on the channel. + Invites map[string]MaskInfo } // ChannelRegistry manages registered channels. @@ -180,11 +180,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC modeSlice[i] = modes.Mode(mode) } - var banlist []string + var banlist map[string]MaskInfo _ = json.Unmarshal([]byte(banlistString), &banlist) - var exceptlist []string + var exceptlist map[string]MaskInfo _ = json.Unmarshal([]byte(exceptlistString), &exceptlist) - var invitelist []string + var invitelist map[string]MaskInfo _ = json.Unmarshal([]byte(invitelistString), &invitelist) accountToUMode := make(map[string]modes.Mode) _ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode) @@ -198,9 +198,9 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC TopicSetTime: time.Unix(topicSetTimeInt, 0), Key: password, Modes: modeSlice, - Banlist: banlist, - Exceptlist: exceptlist, - Invitelist: invitelist, + Bans: banlist, + Excepts: exceptlist, + Invites: invitelist, AccountToUMode: accountToUMode, } return nil @@ -296,11 +296,11 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha } if includeFlags&IncludeLists != 0 { - banlistString, _ := json.Marshal(channelInfo.Banlist) + banlistString, _ := json.Marshal(channelInfo.Bans) tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil) - exceptlistString, _ := json.Marshal(channelInfo.Exceptlist) + exceptlistString, _ := json.Marshal(channelInfo.Excepts) tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil) - invitelistString, _ := json.Marshal(channelInfo.Invitelist) + invitelistString, _ := json.Marshal(channelInfo.Invites) tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil) accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode) tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil) diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 35e7b41a..edf792fe 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -5,10 +5,9 @@ package irc import ( - "fmt" - "log" "regexp" "strings" + "time" "github.com/goshuirc/irc-go/ircmatch" @@ -251,32 +250,42 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) { //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 - masks map[string]bool + masks map[string]MaskInfo regexp *regexp.Regexp } -// NewUserMaskSet returns a new UserMaskSet. func NewUserMaskSet() *UserMaskSet { - return &UserMaskSet{ - masks: make(map[string]bool), - } + return new(UserMaskSet) } // Add adds the given mask to this set. -func (set *UserMaskSet) Add(mask string) (added bool) { - casefoldedMask, err := Casefold(mask) +func (set *UserMaskSet) Add(mask, creatorNickmask, creatorAccount string) (added bool) { + casefoldedMask, err := CanonicalizeMaskWildcard(mask) if err != nil { - log.Println(fmt.Sprintf("ERROR: Could not add mask to usermaskset: [%s]", mask)) return false } set.Lock() - added = !set.masks[casefoldedMask] + if set.masks == nil { + set.masks = make(map[string]MaskInfo) + } + _, present := set.masks[casefoldedMask] + added = !present if added { - set.masks[casefoldedMask] = true + set.masks[casefoldedMask] = MaskInfo{ + TimeCreated: time.Now().UTC(), + CreatorNickmask: creatorNickmask, + CreatorAccount: creatorAccount, + } } set.Unlock() @@ -286,27 +295,15 @@ func (set *UserMaskSet) Add(mask string) (added bool) { return } -// AddAll adds the given masks to this set. -func (set *UserMaskSet) AddAll(masks []string) (added bool) { - set.Lock() - defer set.Unlock() - - for _, mask := range masks { - if !added && !set.masks[mask] { - added = true - } - set.masks[mask] = true - } - if added { - set.setRegexp() - } - return -} - // Remove removes the given mask from this set. func (set *UserMaskSet) Remove(mask string) (removed bool) { + mask, err := CanonicalizeMaskWildcard(mask) + if err != nil { + return false + } + set.Lock() - removed = set.masks[mask] + _, removed = set.masks[mask] if removed { delete(set.masks, mask) } @@ -318,6 +315,24 @@ func (set *UserMaskSet) Remove(mask string) (removed bool) { 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() @@ -330,19 +345,6 @@ func (set *UserMaskSet) Match(userhost string) bool { return regexp.MatchString(userhost) } -// String returns the masks in this set. -func (set *UserMaskSet) String() string { - set.RLock() - masks := make([]string, len(set.masks)) - index := 0 - for mask := range set.masks { - masks[index] = mask - index++ - } - set.RUnlock() - return strings.Join(masks, " ") -} - func (set *UserMaskSet) Length() int { set.RLock() defer set.RUnlock() diff --git a/irc/database.go b/irc/database.go index 62e478b6..eddfab6c 100644 --- a/irc/database.go +++ b/irc/database.go @@ -22,7 +22,7 @@ const ( // 'version' of the database schema keySchemaVersion = "db.version" // latest schema of the db - latestDbSchema = "6" + latestDbSchema = "7" ) type SchemaChanger func(*Config, *buntdb.Tx) error @@ -440,6 +440,66 @@ func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error { return nil } +type maskInfoV7 struct { + TimeCreated time.Time + CreatorNickmask string + CreatorAccount string +} + +func schemaChangeV6ToV7(config *Config, tx *buntdb.Tx) error { + now := time.Now().UTC() + var channels []string + prefix := "channel.exists " + tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + channels = append(channels, strings.TrimPrefix(key, prefix)) + return true + }) + + converter := func(key string) { + oldRawValue, err := tx.Get(key) + if err != nil { + return + } + var masks []string + err = json.Unmarshal([]byte(oldRawValue), &masks) + if err != nil { + return + } + newCookedValue := make(map[string]maskInfoV7) + for _, mask := range masks { + normalizedMask, err := CanonicalizeMaskWildcard(mask) + if err != nil { + continue + } + newCookedValue[normalizedMask] = maskInfoV7{ + TimeCreated: now, + CreatorNickmask: "*", + CreatorAccount: "*", + } + } + newRawValue, err := json.Marshal(newCookedValue) + if err != nil { + return + } + tx.Set(key, string(newRawValue), nil) + } + + prefixes := []string{ + "channel.banlist %s", + "channel.exceptlist %s", + "channel.invitelist %s", + } + for _, channel := range channels { + for _, prefix := range prefixes { + converter(fmt.Sprintf(prefix, channel)) + } + } + return nil +} + func init() { allChanges := []SchemaChange{ { @@ -467,6 +527,11 @@ func init() { TargetVersion: "6", Changer: schemaChangeV5ToV6, }, + { + InitialVersion: "6", + TargetVersion: "7", + Changer: schemaChangeV6ToV7, + }, } // build the index diff --git a/irc/handlers.go b/irc/handlers.go index 297d258b..66329f4b 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1738,7 +1738,7 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res } else { args := append([]string{client.nick, channel.name}, channel.modeStrings(client)...) rb.Add(nil, prefix, RPL_CHANNELMODEIS, args...) - rb.Add(nil, client.nickMaskString, RPL_CHANNELCREATED, client.nick, channel.name, strconv.FormatInt(channel.createdTime.Unix(), 10)) + rb.Add(nil, client.nickMaskString, RPL_CREATIONTIME, client.nick, channel.name, strconv.FormatInt(channel.createdTime.Unix(), 10)) } return false } diff --git a/irc/modes.go b/irc/modes.go index 33db0546..b05eb6ee 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -158,12 +158,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c continue } - // confirm mask looks valid - mask, err := Casefold(change.Arg) - if err != nil { - continue - } - + mask := change.Arg switch change.Op { case modes.Add: if channel.lists[change.Mode].Length() >= client.server.Config().Limits.ChanListModes { @@ -174,12 +169,15 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c continue } - channel.lists[change.Mode].Add(mask) - applied = append(applied, change) + details := client.Details() + if success := channel.lists[change.Mode].Add(mask, details.nickMask, details.accountName); success { + applied = append(applied, change) + } case modes.Remove: - channel.lists[change.Mode].Remove(mask) - applied = append(applied, change) + if success := channel.lists[change.Mode].Remove(mask); success { + applied = append(applied, change) + } } case modes.UserLimit: diff --git a/irc/numerics.go b/irc/numerics.go index 90218bfb..640bff50 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -70,7 +70,7 @@ const ( RPL_LISTEND = "323" RPL_CHANNELMODEIS = "324" RPL_UNIQOPIS = "325" - RPL_CHANNELCREATED = "329" + RPL_CREATIONTIME = "329" RPL_WHOISACCOUNT = "330" RPL_NOTOPIC = "331" RPL_TOPIC = "332" diff --git a/irc/strings.go b/irc/strings.go index 26a000c6..67c144d1 100644 --- a/irc/strings.go +++ b/irc/strings.go @@ -8,6 +8,7 @@ package irc import ( "fmt" "strings" + "unicode" "github.com/oragono/confusables" "golang.org/x/text/cases" @@ -191,9 +192,15 @@ func CanonicalizeMaskWildcard(userhost string) (expanded string, err error) { nick = "*" } if nick != "*" { - nick, err = Casefold(nick) - if err != nil { - return "", err + // XXX allow nick wildcards in pure ASCII, but not in unicode, + // because the * character breaks casefolding + if IsPureASCII(nick) { + nick = strings.ToLower(nick) + } else { + nick, err = Casefold(nick) + if err != nil { + return "", err + } } } if user == "" { @@ -210,3 +217,12 @@ func CanonicalizeMaskWildcard(userhost string) (expanded string, err error) { } return fmt.Sprintf("%s!%s@%s", nick, user, host), nil } + +func IsPureASCII(str string) bool { + for i := 0; i < len(str); i++ { + if unicode.MaxASCII < str[i] { + return false + } + } + return true +} diff --git a/irc/strings_test.go b/irc/strings_test.go index f71d5929..f3d9aeb0 100644 --- a/irc/strings_test.go +++ b/irc/strings_test.go @@ -211,4 +211,5 @@ func TestCanonicalizeMaskWildcard(t *testing.T) { tester("slingamn!shivaram*", "slingamn!shivaram*@*", nil) tester("slingamn!", "slingamn!*@*", nil) tester("shivaram*@good-fortune", "*!shivaram*@good-fortune", nil) + tester("shivaram*", "shivaram*!*@*", nil) }