diff --git a/Makefile b/Makefile index e69da6c4..2be9474a 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ test: cd irc/modes && go test . && go vet . cd irc/mysql && go test . && go vet . cd irc/passwd && go test . && go vet . + cd irc/sno && go test . && go vet . cd irc/utils && go test . && go vet . ./.check-gofmt.sh diff --git a/default.yaml b/default.yaml index b88a388d..d101396b 100644 --- a/default.yaml +++ b/default.yaml @@ -593,6 +593,7 @@ oper-classes: - "vhosts" - "sajoin" - "samode" + - "snomasks" # server admin: has full control of the ircd, including nickname and # channel registrations diff --git a/irc/channel.go b/irc/channel.go index cd0edb34..e9e47c78 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -439,7 +439,7 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) { channel.stateMutex.RLock() clientData, isJoined := channel.members[client] channel.stateMutex.RUnlock() - isOper := client.HasMode(modes.Operator) + isOper := client.HasRoleCapabs("sajoin") respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper && (!isJoined || clientData.modes.HighestChannelUserMode() == modes.Mode(0)) isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix) @@ -607,7 +607,7 @@ func (channel *Channel) hasClient(client *Client) bool { // func (channel *Channel) modeStrings(client *Client) (result []string) { - hasPrivs := client.HasMode(modes.Operator) + hasPrivs := client.HasRoleCapabs("sajoin") channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() @@ -1245,12 +1245,12 @@ func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer, sendNoTopi // SetTopic sets the topic of this channel, if the client is allowed to do so. func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffer) { - if !(client.HasMode(modes.Operator) || channel.hasClient(client)) { + if !channel.hasClient(client) { rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel")) return } - if channel.flags.HasMode(modes.OpOnlyTopic) && !channel.ClientIsAtLeast(client, modes.Halfop) { + if channel.flags.HasMode(modes.OpOnlyTopic) && !(channel.ClientIsAtLeast(client, modes.Halfop) || client.HasRoleCapabs("samode")) { rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You're not a channel operator")) return } @@ -1487,10 +1487,6 @@ func (channel *Channel) Quit(client *Client) { func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer, hasPrivs bool) { if !hasPrivs { - if !(client.HasMode(modes.Operator) || channel.hasClient(client)) { - rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel")) - return - } if !channel.ClientHasPrivsOver(client, target) { rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You don't have enough channel privileges")) return diff --git a/irc/chanserv.go b/irc/chanserv.go index 46e49d1d..3f912ebd 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -876,7 +876,7 @@ func csHowToBanHandler(service *ircService, server *Server, client *Client, comm return } - if !(channel.ClientIsAtLeast(client, modes.Operator) || client.HasRoleCapabs("samode")) { + if !(channel.ClientIsAtLeast(client, modes.ChannelOperator) || client.HasRoleCapabs("samode")) { service.Notice(rb, client.t("Insufficient privileges")) return } diff --git a/irc/client.go b/irc/client.go index abe05aff..b39d2985 100644 --- a/irc/client.go +++ b/irc/client.go @@ -1512,7 +1512,7 @@ func (client *Client) destroy(session *Session) { // decrement stats if we have no more sessions, even if the client will not be destroyed if shouldDecrement { invisible := client.HasMode(modes.Invisible) - operator := client.HasMode(modes.LocalOperator) || client.HasMode(modes.Operator) + operator := client.HasMode(modes.Operator) client.server.stats.Remove(registered, invisible, operator) } diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 61e257a6..230b69da 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -222,7 +222,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick } if numSessions == 1 { invisible := currentClient.HasMode(modes.Invisible) - operator := currentClient.HasMode(modes.Operator) || currentClient.HasMode(modes.LocalOperator) + operator := currentClient.HasMode(modes.Operator) client.server.stats.AddRegistered(invisible, operator) } session.autoreplayMissedSince = lastSeen diff --git a/irc/commands.go b/irc/commands.go index 9d0ae58d..7fa70287 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -7,13 +7,11 @@ package irc import ( "github.com/goshuirc/irc-go/ircmsg" - "github.com/oragono/oragono/irc/modes" ) // 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 allowedInBatch bool // allowed in client-to-server batches minParams int @@ -32,10 +30,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir rb.Add(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command")) return false } - if cmd.oper && !client.HasMode(modes.Operator) { - rb.Add(nil, server.name, ERR_NOPRIVILEGES, client.Nick(), client.t("Permission Denied - You're not an IRC operator")) - return false - } if len(cmd.capabs) > 0 && !client.HasRoleCapabs(cmd.capabs...) { rb.Add(nil, server.name, ERR_NOPRIVILEGES, client.Nick(), client.t("Permission Denied")) return false @@ -115,7 +109,7 @@ func init() { "DEBUG": { handler: debugHandler, minParams: 1, - oper: true, + capabs: []string{"rehash"}, }, "DEFCON": { handler: defconHandler, @@ -124,12 +118,11 @@ func init() { "DEOPER": { handler: deoperHandler, minParams: 0, - oper: true, }, "DLINE": { handler: dlineHandler, minParams: 1, - oper: true, + capabs: []string{"ban"}, }, "EXTJWT": { handler: extjwtHandler, @@ -169,13 +162,12 @@ func init() { "KILL": { handler: killHandler, minParams: 1, - oper: true, capabs: []string{"kill"}, }, "KLINE": { handler: klineHandler, minParams: 1, - oper: true, + capabs: []string{"ban"}, }, "LANGUAGE": { handler: languageHandler, @@ -278,7 +270,7 @@ func init() { "SANICK": { handler: sanickHandler, minParams: 2, - oper: true, + capabs: []string{"samode"}, }, "SAMODE": { handler: modeHandler, @@ -308,7 +300,6 @@ func init() { "REHASH": { handler: rehashHandler, minParams: 0, - oper: true, capabs: []string{"rehash"}, }, "TIME": { @@ -327,7 +318,7 @@ func init() { "UNDLINE": { handler: unDLineHandler, minParams: 1, - oper: true, + capabs: []string{"ban"}, }, "UNINVITE": { handler: inviteHandler, @@ -336,7 +327,7 @@ func init() { "UNKLINE": { handler: unKLineHandler, minParams: 1, - oper: true, + capabs: []string{"ban"}, }, "USER": { handler: userHandler, diff --git a/irc/handlers.go b/irc/handlers.go index 2c3bcf74..110dfe20 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -757,10 +757,6 @@ func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res rb.Notice(fmt.Sprintf("CPU profiling stopped")) case "CRASHSERVER": - if !client.HasRoleCapabs("rehash") { - rb.Notice(client.t("You must have rehash permissions in order to execute DEBUG CRASHSERVER")) - return false - } code := utils.ConfirmationCode(server.name, server.ctime) if len(msg.Params) == 1 || msg.Params[1] != code { rb.Notice(fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/DEBUG CRASHSERVER %s", code))) @@ -1293,6 +1289,7 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re // KICK {,} {,} [] func kickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + hasPrivs := client.HasRoleCapabs("samode") channels := strings.Split(msg.Params[0], ",") users := strings.Split(msg.Params[1], ",") if (len(channels) != len(users)) && (len(users) != 1) { @@ -1336,7 +1333,7 @@ func kickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp if comment == "" { comment = kick.nick } - channel.Kick(client, target, comment, rb, false) + channel.Kick(client, target, comment, rb, hasPrivs) } return false } @@ -1618,7 +1615,7 @@ func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp rb.Add(nil, client.server.name, RPL_LIST, nick, name, strconv.Itoa(members), topic) } - clientIsOp := client.HasMode(modes.Operator) + clientIsOp := client.HasRoleCapabs("sajoin") if len(channels) == 0 { for _, channel := range server.channels.Channels() { if !clientIsOp && channel.flags.HasMode(modes.Secret) { @@ -1775,7 +1772,7 @@ func umodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res rb.Add(nil, cDetails.nickMask, "MODE", args...) } else if hasPrivs { rb.Add(nil, server.name, RPL_UMODEIS, targetNick, target.ModeString()) - if target.HasMode(modes.LocalOperator) || target.HasMode(modes.Operator) { + if target.HasMode(modes.Operator) { masks := server.snomasks.String(target) if 0 < len(masks) { rb.Add(nil, server.name, RPL_SNOMASKIS, targetNick, masks, client.t("Server notice masks")) @@ -1959,7 +1956,7 @@ func namesHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res success := false channel := server.channels.Get(chname) if channel != nil { - if !channel.flags.HasMode(modes.Secret) || channel.hasClient(client) || client.HasMode(modes.Operator) { + if !channel.flags.HasMode(modes.Secret) || channel.hasClient(client) || client.HasRoleCapabs("sajoin") { channel.Names(client, rb) success = true } @@ -2338,6 +2335,10 @@ func applyOper(client *Client, oper *Oper, rb *ResponseBuffer) { // DEOPER func deoperHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + if client.Oper() == nil { + rb.Notice(client.t("Insufficient oper privs")) + return false + } // pretend they sent /MODE $nick -o fakeModeMsg := ircmsg.MakeMessage(nil, "", "MODE", client.Nick(), "-o") return umodeHandler(server, client, fakeModeMsg, rb) @@ -2944,7 +2945,7 @@ func operStatusVisible(client, target *Client, hasPrivs bool) bool { // USERHOST { } func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - hasPrivs := client.HasMode(modes.Operator) // TODO(#1176) figure out the right capab for this + hasPrivs := client.HasMode(modes.Operator) returnedClients := make(ClientSet) var tl utils.TokenLineBuilder @@ -3083,7 +3084,7 @@ func (fields whoxFields) Has(field rune) bool { // [*][~|&|@|%|+][B] : // whox format: // [*][~|&|@|%|+][B] : -func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *ResponseBuffer, hasPrivs, includeRFlag, isWhox bool, fields whoxFields, whoType string) { +func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *ResponseBuffer, canSeeIPs, canSeeOpers, includeRFlag, isWhox bool, fields whoxFields, whoType string) { params := []string{client.Nick()} details := target.Details() @@ -3103,7 +3104,7 @@ func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *Response } if fields.Has('i') { fIP := "255.255.255.255" - if hasPrivs || client == target { + if canSeeIPs || client == target { // you can only see a target's IP if they're you or you're an oper fIP = target.IPString() } @@ -3126,7 +3127,7 @@ func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *Response flags.WriteRune('H') // Here } - if target.HasMode(modes.Operator) && operStatusVisible(client, target, hasPrivs) { + if target.HasMode(modes.Operator) && operStatusVisible(client, target, canSeeOpers) { flags.WriteRune('*') } @@ -3229,23 +3230,23 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo // operatorOnly = true //} - isOper := client.HasMode(modes.Operator) + oper := client.Oper() + hasPrivs := oper.HasRoleCapab("sajoin") + canSeeIPs := oper.HasRoleCapab("ban") if mask[0] == '#' { - // TODO implement wildcard matching - //TODO(dan): ^ only for opers channel := server.channels.Get(mask) if channel != nil { isJoined := channel.hasClient(client) - if !channel.flags.HasMode(modes.Secret) || isJoined || isOper { + if !channel.flags.HasMode(modes.Secret) || isJoined || hasPrivs { var members []*Client - if isOper { + if hasPrivs { members = channel.Members() } else { members = channel.auditoriumFriends(client) } for _, member := range members { - if !member.HasMode(modes.Invisible) || isJoined || isOper { - client.rplWhoReply(channel, member, rb, isOper, includeRFlag, isWhox, fields, whoType) + if !member.HasMode(modes.Invisible) || isJoined || hasPrivs { + client.rplWhoReply(channel, member, rb, canSeeIPs, oper != nil, includeRFlag, isWhox, fields, whoType) } } } @@ -3275,8 +3276,8 @@ 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, isOper, includeRFlag, isWhox, fields, whoType) + if hasPrivs || !mclient.HasMode(modes.Invisible) || isFriend(mclient) { + client.rplWhoReply(nil, mclient, rb, canSeeIPs, oper != nil, includeRFlag, isWhox, fields, whoType) } } } @@ -3319,7 +3320,7 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res return true } - hasPrivs := client.HasMode(modes.Operator) // TODO(#1176) figure out the right capab for this + hasPrivs := client.HasRoleCapabs("samode") if hasPrivs { for _, mask := range strings.Split(masksString, ",") { matches := server.clients.FindAll(mask) diff --git a/irc/modes.go b/irc/modes.go index 1fd27e19..f8a1e85d 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -37,14 +37,14 @@ func ApplyUserModeChanges(client *Client, changes modes.ModeChanges, force bool, if change.Mode != modes.ServerNotice { switch change.Op { case modes.Add: - if (change.Mode == modes.Operator || change.Mode == modes.LocalOperator) && !(force && oper != nil) { + if (change.Mode == modes.Operator) && !(force && oper != nil) { continue } if client.SetMode(change.Mode, true) { if change.Mode == modes.Invisible { client.server.stats.ChangeInvisible(1) - } else if change.Mode == modes.Operator || change.Mode == modes.LocalOperator { + } else if change.Mode == modes.Operator { client.server.stats.ChangeOperators(1) } applied = append(applied, change) @@ -55,7 +55,7 @@ func ApplyUserModeChanges(client *Client, changes modes.ModeChanges, force bool, if client.SetMode(change.Mode, false) { if change.Mode == modes.Invisible { client.server.stats.ChangeInvisible(-1) - } else if change.Mode == modes.Operator || change.Mode == modes.LocalOperator { + } else if change.Mode == modes.Operator { removedSnomasks = client.server.snomasks.String(client) client.server.stats.ChangeOperators(-1) applyOper(client, nil, nil) @@ -75,26 +75,28 @@ func ApplyUserModeChanges(client *Client, changes modes.ModeChanges, force bool, } } else { // server notices are weird - if !client.HasMode(modes.Operator) { + if !client.HasMode(modes.Operator) || change.Op == modes.List { continue } - var masks []sno.Mask - if change.Op == modes.Add || change.Op == modes.Remove { - var newArg string - for _, char := range change.Arg { - mask := sno.Mask(char) - if sno.ValidMasks[mask] { - masks = append(masks, mask) - newArg += string(char) - } + + currentMasks := client.server.snomasks.MasksEnabled(client) + addMasks, removeMasks, newArg := sno.EvaluateSnomaskChanges(change.Op == modes.Add, change.Arg, currentMasks) + + success := false + if len(addMasks) != 0 { + oper := client.Oper() + // #1176: require special operator privileges to subscribe to snomasks + if oper.HasRoleCapab("snomasks") || oper.HasRoleCapab("ban") { + success = true + client.server.snomasks.AddMasks(client, addMasks...) } - change.Arg = newArg } - if change.Op == modes.Add { - client.server.snomasks.AddMasks(client, masks...) - applied = append(applied, change) - } else if change.Op == modes.Remove { - client.server.snomasks.RemoveMasks(client, masks...) + if len(removeMasks) != 0 { + success = true + client.server.snomasks.RemoveMasks(client, removeMasks...) + } + if success { + change.Arg = newArg applied = append(applied, change) } } diff --git a/irc/modes/modes.go b/irc/modes/modes.go index 8019c3f9..f91cc06b 100644 --- a/irc/modes/modes.go +++ b/irc/modes/modes.go @@ -101,7 +101,6 @@ func (modes Modes) String() string { const ( Bot Mode = 'B' Invisible Mode = 'i' - LocalOperator Mode = 'O' Operator Mode = 'o' Restricted Mode = 'r' RegisteredOnly Mode = 'R' @@ -213,12 +212,10 @@ func ParseUserModeChanges(params ...string) (ModeChanges, map[rune]bool) { // put arg into modechange if needed switch Mode(mode) { case ServerNotice: - // always require arg + // arg is optional for ServerNotice (we accept bare `-s`) if len(params) > skipArgs { change.Arg = params[skipArgs] skipArgs++ - } else { - continue } } diff --git a/irc/modes/modes_test.go b/irc/modes/modes_test.go index 27f04888..67d28c2f 100644 --- a/irc/modes/modes_test.go +++ b/irc/modes/modes_test.go @@ -15,6 +15,38 @@ func assertEqual(supplied, expected interface{}, t *testing.T) { } } +func TestParseUserModeChanges(t *testing.T) { + emptyUnknown := make(map[rune]bool) + changes, unknown := ParseUserModeChanges("+i") + assertEqual(unknown, emptyUnknown, t) + assertEqual(changes, ModeChanges{ModeChange{Op: Add, Mode: Invisible}}, t) + + // no-op change to sno + changes, unknown = ParseUserModeChanges("+is") + assertEqual(unknown, emptyUnknown, t) + assertEqual(changes, ModeChanges{ModeChange{Op: Add, Mode: Invisible}, ModeChange{Op: Add, Mode: ServerNotice}}, t) + + // add snomasks + changes, unknown = ParseUserModeChanges("+is", "ac") + assertEqual(unknown, emptyUnknown, t) + assertEqual(changes, ModeChanges{ModeChange{Op: Add, Mode: Invisible}, ModeChange{Op: Add, Mode: ServerNotice, Arg: "ac"}}, t) + + // remove snomasks + changes, unknown = ParseUserModeChanges("+s", "-cx") + assertEqual(unknown, emptyUnknown, t) + assertEqual(changes, ModeChanges{ModeChange{Op: Add, Mode: ServerNotice, Arg: "-cx"}}, t) + + // remove all snomasks (arg is parsed but has no meaning) + changes, unknown = ParseUserModeChanges("-is", "ac") + assertEqual(unknown, emptyUnknown, t) + assertEqual(changes, ModeChanges{ModeChange{Op: Remove, Mode: Invisible}, ModeChange{Op: Remove, Mode: ServerNotice, Arg: "ac"}}, t) + + // remove all snomasks + changes, unknown = ParseUserModeChanges("-is") + assertEqual(unknown, emptyUnknown, t) + assertEqual(changes, ModeChanges{ModeChange{Op: Remove, Mode: Invisible}, ModeChange{Op: Remove, Mode: ServerNotice}}, t) +} + func TestIssue874(t *testing.T) { emptyUnknown := make(map[rune]bool) modes, unknown := ParseChannelModeChanges("+k") diff --git a/irc/server.go b/irc/server.go index ed74f60f..d18787cf 100644 --- a/irc/server.go +++ b/irc/server.go @@ -459,15 +459,13 @@ func (server *Server) MOTD(client *Client, rb *ResponseBuffer) { rb.Add(nil, server.name, RPL_ENDOFMOTD, client.nick, client.t("End of MOTD command")) } -// WhoisChannelsNames returns the common channel names between two users. -func (client *Client) WhoisChannelsNames(target *Client, multiPrefix bool) []string { +func (client *Client) whoisChannelsNames(target *Client, multiPrefix bool, hasPrivs bool) []string { var chstrs []string + targetInvis := target.HasMode(modes.Invisible) for _, channel := range target.Channels() { - // channel is secret and the target can't see it - if !client.HasMode(modes.Operator) { - if (target.HasMode(modes.Invisible) || channel.flags.HasMode(modes.Secret)) && !channel.hasClient(client) { - continue - } + if !hasPrivs && (targetInvis || channel.flags.HasMode(modes.Secret)) && !channel.hasClient(client) { + // client can't see *this* channel membership + continue } chstrs = append(chstrs, channel.ClientPrefixes(target, multiPrefix)+channel.name) } @@ -475,23 +473,26 @@ func (client *Client) WhoisChannelsNames(target *Client, multiPrefix bool) []str } func (client *Client) getWhoisOf(target *Client, hasPrivs bool, rb *ResponseBuffer) { + oper := client.Oper() cnick := client.Nick() targetInfo := target.Details() rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nick, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname) tnick := targetInfo.nick - whoischannels := client.WhoisChannelsNames(target, rb.session.capabilities.Has(caps.MultiPrefix)) + whoischannels := client.whoisChannelsNames(target, rb.session.capabilities.Has(caps.MultiPrefix), oper.HasRoleCapab("sajoin")) if whoischannels != nil { rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, cnick, tnick, strings.Join(whoischannels, " ")) } - if target.HasMode(modes.Operator) && operStatusVisible(client, target, hasPrivs) { + if target.HasMode(modes.Operator) && operStatusVisible(client, target, oper != nil) { tOper := target.Oper() if tOper != nil { rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, cnick, tnick, tOper.WhoisLine) } } - if client == target || hasPrivs { + if client == target || oper.HasRoleCapab("ban") { rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, cnick, tnick, fmt.Sprintf("%s@%s", targetInfo.username, target.RawHostname()), target.IPString(), client.t("Actual user@host, Actual IP")) + } + if client == target || oper.HasRoleCapab("samode") { rb.Add(nil, client.server.name, RPL_WHOISMODES, cnick, tnick, fmt.Sprintf(client.t("is using modes +%s"), target.modes.String())) } if target.HasMode(modes.TLS) { @@ -504,7 +505,7 @@ func (client *Client) getWhoisOf(target *Client, hasPrivs bool, rb *ResponseBuff rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, fmt.Sprintf(ircfmt.Unescape(client.t("is a $bBot$b on %s")), client.server.Config().Network.Name)) } - if client == target || hasPrivs { + if client == target || oper.HasRoleCapab("ban") { for _, session := range target.Sessions() { if session.certfp != "" { rb.Add(nil, client.server.name, RPL_WHOISCERTFP, cnick, tnick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), session.certfp)) diff --git a/irc/sno/constants.go b/irc/sno/constants.go index 1eb1bc5a..542ab57f 100644 --- a/irc/sno/constants.go +++ b/irc/sno/constants.go @@ -7,6 +7,8 @@ package sno // Mask is a type of server notice mask. type Mask rune +type Masks []Mask + // Notice mask types const ( LocalAnnouncements Mask = 'a' @@ -18,8 +20,8 @@ const ( LocalQuits Mask = 'q' Stats Mask = 't' LocalAccounts Mask = 'u' - LocalXline Mask = 'x' LocalVhosts Mask = 'v' + LocalXline Mask = 'x' ) var ( @@ -39,17 +41,17 @@ var ( } // ValidMasks contains the snomasks that we support. - ValidMasks = map[Mask]bool{ - LocalAnnouncements: true, - LocalConnects: true, - LocalChannels: true, - LocalKills: true, - LocalNicks: true, - LocalOpers: true, - LocalQuits: true, - Stats: true, - LocalAccounts: true, - LocalXline: true, - LocalVhosts: true, + ValidMasks = []Mask{ + LocalAnnouncements, + LocalConnects, + LocalChannels, + LocalKills, + LocalNicks, + LocalOpers, + LocalQuits, + Stats, + LocalAccounts, + LocalVhosts, + LocalXline, } ) diff --git a/irc/sno/utils.go b/irc/sno/utils.go new file mode 100644 index 00000000..572ab394 --- /dev/null +++ b/irc/sno/utils.go @@ -0,0 +1,87 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package sno + +import ( + "strings" +) + +func IsValidMask(r rune) bool { + for _, m := range ValidMasks { + if m == Mask(r) { + return true + } + } + return false +} + +func (masks Masks) String() string { + var buf strings.Builder + buf.Grow(len(masks)) + for _, m := range masks { + buf.WriteRune(rune(m)) + } + return buf.String() +} + +func (masks Masks) Contains(mask Mask) bool { + for _, m := range masks { + if mask == m { + return true + } + } + return false +} + +// Evaluate changes to snomasks made with MODE. There are several cases: +// adding snomasks with `/mode +s a` or `/mode +s +a`, removing them with `/mode +s -a`, +// adding all with `/mode +s *` or `/mode +s +*`, removing all with `/mode +s -*` or `/mode -s` +func EvaluateSnomaskChanges(add bool, arg string, currentMasks Masks) (addMasks, removeMasks Masks, newArg string) { + if add { + if len(arg) == 0 { + return + } + add := true + switch arg[0] { + case '+': + arg = arg[1:] + case '-': + add = false + arg = arg[1:] + default: + // add + } + if strings.IndexByte(arg, '*') != -1 { + if add { + for _, mask := range ValidMasks { + if !currentMasks.Contains(mask) { + addMasks = append(addMasks, mask) + } + } + } else { + removeMasks = currentMasks + } + } else { + for _, r := range arg { + if IsValidMask(r) { + m := Mask(r) + if add && !currentMasks.Contains(m) { + addMasks = append(addMasks, m) + } else if !add && currentMasks.Contains(m) { + removeMasks = append(removeMasks, m) + } + } + } + } + if len(addMasks) != 0 { + newArg = "+" + addMasks.String() + } else if len(removeMasks) != 0 { + newArg = "-" + removeMasks.String() + } + } else { + removeMasks = currentMasks + newArg = "" + } + return +} diff --git a/irc/sno/utils_test.go b/irc/sno/utils_test.go new file mode 100644 index 00000000..46ce30bf --- /dev/null +++ b/irc/sno/utils_test.go @@ -0,0 +1,53 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package sno + +import ( + "fmt" + "reflect" + "testing" +) + +func assertEqual(supplied, expected interface{}, t *testing.T) { + if !reflect.DeepEqual(supplied, expected) { + panic(fmt.Sprintf("expected %#v but got %#v", expected, supplied)) + } +} + +func TestEvaluateSnomaskChanges(t *testing.T) { + add, remove, newArg := EvaluateSnomaskChanges(true, "*", nil) + assertEqual(add, Masks{'a', 'c', 'j', 'k', 'n', 'o', 'q', 't', 'u', 'v', 'x'}, t) + assertEqual(len(remove), 0, t) + assertEqual(newArg, "+acjknoqtuvx", t) + + add, remove, newArg = EvaluateSnomaskChanges(true, "*", Masks{'a', 'u'}) + assertEqual(add, Masks{'c', 'j', 'k', 'n', 'o', 'q', 't', 'v', 'x'}, t) + assertEqual(len(remove), 0, t) + assertEqual(newArg, "+cjknoqtvx", t) + + add, remove, newArg = EvaluateSnomaskChanges(true, "-a", Masks{'a', 'u'}) + assertEqual(len(add), 0, t) + assertEqual(remove, Masks{'a'}, t) + assertEqual(newArg, "-a", t) + + add, remove, newArg = EvaluateSnomaskChanges(true, "-*", Masks{'a', 'u'}) + assertEqual(len(add), 0, t) + assertEqual(remove, Masks{'a', 'u'}, t) + assertEqual(newArg, "-au", t) + + add, remove, newArg = EvaluateSnomaskChanges(true, "+c", Masks{'a', 'u'}) + assertEqual(add, Masks{'c'}, t) + assertEqual(len(remove), 0, t) + assertEqual(newArg, "+c", t) + + add, remove, newArg = EvaluateSnomaskChanges(false, "", Masks{'a', 'u'}) + assertEqual(len(add), 0, t) + assertEqual(remove, Masks{'a', 'u'}, t) + assertEqual(newArg, "", t) + + add, remove, newArg = EvaluateSnomaskChanges(false, "*", Masks{'a', 'u'}) + assertEqual(len(add), 0, t) + assertEqual(remove, Masks{'a', 'u'}, t) + assertEqual(newArg, "", t) +} diff --git a/irc/snomanager.go b/irc/snomanager.go index b7e48107..0bd394bc 100644 --- a/irc/snomanager.go +++ b/irc/snomanager.go @@ -24,11 +24,6 @@ func (m *SnoManager) AddMasks(client *Client, masks ...sno.Mask) { defer m.sendListMutex.Unlock() for _, mask := range masks { - // confirm mask is valid - if !sno.ValidMasks[mask] { - continue - } - currentClientList := m.sendLists[mask] if currentClientList == nil { @@ -101,19 +96,23 @@ func (m *SnoManager) Send(mask sno.Mask, content string) { } } -// String returns the snomasks currently enabled. -func (m *SnoManager) String(client *Client) string { +// MasksEnabled returns the snomasks currently enabled. +func (m *SnoManager) MasksEnabled(client *Client) (result sno.Masks) { m.sendListMutex.RLock() defer m.sendListMutex.RUnlock() - var masks string for mask, clients := range m.sendLists { for c := range clients { if c == client { - masks += string(mask) + result = append(result, mask) break } } } - return masks + return +} + +func (m *SnoManager) String(client *Client) string { + masks := m.MasksEnabled(client) + return masks.String() } diff --git a/traditional.yaml b/traditional.yaml index d3a8ef55..ba9141df 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -565,6 +565,7 @@ oper-classes: - "vhosts" - "sajoin" - "samode" + - "snomasks" # server admin: has full control of the ircd, including nickname and # channel registrations