From fd793d6adb1dd6cd8669406c324f7839c70ffcec Mon Sep 17 00:00:00 2001 From: Daniel Oaks Date: Mon, 8 May 2017 09:15:16 +1000 Subject: [PATCH] Add very initial snomasks --- CHANGELOG.md | 2 + irc/config.go | 3 + irc/modes.go | 131 +++++++++++++++++++++++++++++++++---------- irc/numerics.go | 1 + irc/server.go | 28 +++++++-- irc/sno/constants.go | 35 ++++++++++++ irc/snomanager.go | 117 ++++++++++++++++++++++++++++++++++++++ oragono.yaml | 3 + 8 files changed, 286 insertions(+), 34 deletions(-) create mode 100644 irc/sno/constants.go create mode 100644 irc/snomanager.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 413ca7b8..441631bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ New release of Oragono! ### Config Changes * Added `debug` section containing additional debug settings. +* Added `modes` key on oper config, for setting modes on oper-up. * Added ability to log to `stdout` in logger methods. ### Security @@ -16,6 +17,7 @@ New release of Oragono! ### Added * Added ability to log to stdout. * Added ability to use StackImpact profiling. +* Added initial server notice masks (snomasks). ### Changed * Socket code rewritten to be a lot faster and safer. diff --git a/irc/config.go b/irc/config.go index a76b4efe..dcdcbf22 100644 --- a/irc/config.go +++ b/irc/config.go @@ -94,6 +94,7 @@ type OperConfig struct { Vhost string WhoisLine string `yaml:"whois-line"` Password string + Modes string } func (conf *OperConfig) PasswordBytes() []byte { @@ -323,6 +324,7 @@ type Oper struct { WhoisLine string Vhost string Pass []byte + Modes string } // Operators returns a map of operator configs from the given OperClass and config. @@ -349,6 +351,7 @@ func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error) } else { oper.WhoisLine = class.WhoisLine } + oper.Modes = strings.TrimSpace(opConf.Modes) // successful, attach to list of opers operators[name] = oper diff --git a/irc/modes.go b/irc/modes.go index 117360f8..d3a69932 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/DanielOaks/girc-go/ircmsg" + "github.com/DanielOaks/oragono/irc/sno" "github.com/tidwall/buntdb" ) @@ -97,7 +98,7 @@ const ( LocalOperator Mode = 'O' Operator Mode = 'o' Restricted Mode = 'r' - ServerNotice Mode = 's' // deprecated + ServerNotice Mode = 's' TLS Mode = 'Z' UserRoleplaying Mode = 'E' WallOps Mode = 'w' @@ -105,7 +106,7 @@ const ( var ( SupportedUserModes = Modes{ - Away, Invisible, Operator, UserRoleplaying, + Away, Invisible, Operator, ServerNotice, UserRoleplaying, } // supportedUserModesString acts as a cache for when we introduce users supportedUserModesString = SupportedUserModes.String() @@ -210,15 +211,77 @@ func modeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { return umodeHandler(server, client, msg) } +// ParseUserModeChanges returns the valid changes, and the list of unknown chars. +func ParseUserModeChanges(params ...string) (ModeChanges, map[rune]bool) { + changes := make(ModeChanges, 0) + unknown := make(map[rune]bool) + + if 0 < len(params) { + modeArg := params[0] + op := ModeOp(modeArg[0]) + if (op == Add) || (op == Remove) { + modeArg = modeArg[1:] + } else { + unknown[rune(modeArg[0])] = true + return changes, unknown + } + + skipArgs := 1 + + for _, mode := range modeArg { + if mode == '-' || mode == '+' { + op = ModeOp(mode) + continue + } + change := ModeChange{ + mode: Mode(mode), + op: op, + } + + // put arg into modechange if needed + switch Mode(mode) { + case ServerNotice: + // always require arg + if len(params) > skipArgs { + change.arg = params[skipArgs] + skipArgs++ + } else { + continue + } + } + + var isKnown bool + for _, supportedMode := range SupportedUserModes { + if rune(supportedMode) == mode { + isKnown = true + break + } + } + if !isKnown { + unknown[mode] = true + continue + } + + changes = append(changes, change) + } + } + + return changes, unknown +} + // applyUserModeChanges applies the given changes, and returns the applied changes. -func (client *Client) applyUserModeChanges(changes ModeChanges) ModeChanges { +func (client *Client) applyUserModeChanges(force bool, changes ModeChanges) ModeChanges { applied := make(ModeChanges, 0) for _, change := range changes { switch change.mode { - case Invisible, ServerNotice, WallOps, UserRoleplaying: + case Invisible, WallOps, UserRoleplaying, Operator, LocalOperator: switch change.op { case Add: + if !force && (change.mode == Operator || change.mode == LocalOperator) { + continue + } + if client.flags[change.mode] { continue } @@ -233,12 +296,21 @@ func (client *Client) applyUserModeChanges(changes ModeChanges) ModeChanges { applied = append(applied, change) } - case Operator, LocalOperator: - if change.op == Remove { - if !client.flags[change.mode] { - continue + case ServerNotice: + if !client.flags[Operator] { + continue + } + var masks []sno.Mask + if change.op == Add || change.op == Remove { + for _, char := range change.arg { + masks = append(masks, sno.Mask(char)) } - delete(client.flags, change.mode) + } + if change.op == Add { + client.server.snomasks.AddMasks(client, masks...) + applied = append(applied, change) + } else if change.op == Remove { + client.server.snomasks.RemoveMasks(client, masks...) applied = append(applied, change) } } @@ -272,38 +344,36 @@ func umodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { return false } - // assemble changes - changes := make(ModeChanges, 0) + // applied mode changes applied := make(ModeChanges, 0) - if len(msg.Params) > 1 { - modeArg := msg.Params[1] - op := ModeOp(modeArg[0]) - if (op == Add) || (op == Remove) { - modeArg = modeArg[1:] - } else { - client.Send(nil, server.name, ERR_UNKNOWNMODE, client.nick, string(modeArg[0]), "is an unknown mode character to me") + if 1 < len(msg.Params) { + // parse out real mode changes + params := msg.Params[1:] + changes, unknown := ParseUserModeChanges(params...) + + // alert for unknown mode changes + for char := range unknown { + client.Send(nil, server.name, ERR_UNKNOWNMODE, client.nick, string(char), "is an unknown mode character to me") + } + if len(unknown) == 1 && len(changes) == 0 { return false } - for _, mode := range modeArg { - if mode == '-' || mode == '+' { - op = ModeOp(mode) - continue - } - changes = append(changes, ModeChange{ - mode: Mode(mode), - op: op, - }) - } - - applied = target.applyUserModeChanges(changes) + // apply mode changes + applied = target.applyUserModeChanges(msg.Command == "SAMODE", changes) } if len(applied) > 0 { client.Send(nil, client.nickMaskString, "MODE", target.nick, applied.String()) } else if client == target { client.Send(nil, target.nickMaskString, RPL_UMODEIS, target.nick, target.ModeString()) + if client.flags[LocalOperator] || client.flags[Operator] { + masks := server.snomasks.String(client) + if 0 < len(masks) { + client.Send(nil, target.nickMaskString, RPL_SNOMASKIS, target.nick, masks, "Server notice masks") + } + } } return false } @@ -372,6 +442,7 @@ func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) { } if !isKnown { unknown[mode] = true + continue } changes = append(changes, change) diff --git a/irc/numerics.go b/irc/numerics.go index 2460e6c3..5f90914d 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -17,6 +17,7 @@ const ( RPL_CREATED = "003" RPL_MYINFO = "004" RPL_ISUPPORT = "005" + RPL_SNOMASKIS = "008" RPL_BOUNCE = "010" RPL_TRACELINK = "200" RPL_TRACECONNECTING = "201" diff --git a/irc/server.go b/irc/server.go index ecc6f558..95957920 100644 --- a/irc/server.go +++ b/irc/server.go @@ -23,8 +23,10 @@ import ( "syscall" "time" + "github.com/DanielOaks/girc-go/ircfmt" "github.com/DanielOaks/girc-go/ircmsg" "github.com/DanielOaks/oragono/irc/logger" + "github.com/DanielOaks/oragono/irc/sno" "github.com/tidwall/buntdb" ) @@ -123,6 +125,7 @@ type Server struct { rehashSignal chan os.Signal restAPI *RestAPIConfig signals chan os.Signal + snomasks *SnoManager store *buntdb.DB stsEnabled bool whoWas *WhoWasList @@ -233,6 +236,7 @@ func NewServer(configFilename string, config *Config, logger *logger.Manager) (* rehashSignal: make(chan os.Signal, 1), restAPI: &config.Server.RestAPI, signals: make(chan os.Signal, len(ServerExitSignals)), + snomasks: NewSnoManager(), stsEnabled: config.Server.STS.Enabled, whoWas: NewWhoWasList(config.Limits.WhowasEntries), } @@ -474,6 +478,7 @@ func (server *Server) Run() { } server.logger.Debug("localconnect-ip", fmt.Sprintf("Client connecting from %v", ipaddr)) + // prolly don't need to alert snomasks on this, only on connection reg go NewClient(server, conn.Conn, conn.IsTLS) continue @@ -664,6 +669,7 @@ func (server *Server) tryRegister(c *Client) { // continue registration server.logger.Debug("localconnect", fmt.Sprintf("Client registered [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname)) + server.snomasks.Send(sno.LocalConnects, fmt.Sprintf(ircfmt.Unescape("Client registered $c[grey][$r%s$c[grey]] [u:$r%s$c[grey]] [h:$r%s$c[grey]] [r:$r%s$c[grey]]"), c.nick, c.username, c.rawHostname, c.realname)) c.Register() // send welcome text @@ -1263,13 +1269,27 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { client.updateNickMask() } + // set new modes + var applied ModeChanges + if 0 < len(server.operators[name].Modes) { + modeChanges, unknownChanges := ParseUserModeChanges(strings.Split(server.operators[name].Modes, " ")...) + applied = client.applyUserModeChanges(true, modeChanges) + if 0 < len(unknownChanges) { + var runes string + for r := range unknownChanges { + runes += string(r) + } + client.Notice(fmt.Sprintf("Could not apply mode changes: +%s", runes)) + } + } + client.Send(nil, server.name, RPL_YOUREOPER, client.nick, "You are now an IRC operator") - //TODO(dan): Should this be sent automagically as part of setting the flag/mode? - modech := ModeChanges{ModeChange{ + + applied = append(applied, ModeChange{ mode: Operator, op: Add, - }} - client.Send(nil, server.name, "MODE", client.nick, modech.String()) + }) + client.Send(nil, server.name, "MODE", client.nick, applied.String()) return false } diff --git a/irc/sno/constants.go b/irc/sno/constants.go new file mode 100644 index 00000000..16a3aa55 --- /dev/null +++ b/irc/sno/constants.go @@ -0,0 +1,35 @@ +// Package sno holds Server Notice masks for easy reference. +package sno + +// Mask is a type of server notice mask. +type Mask rune + +// Notice mask types +const ( + LocalAccouncements Mask = 'a' + LocalConnects Mask = 'c' + LocalChannels Mask = 'j' + LocalKills Mask = 'k' + LocalNicks Mask = 'n' + LocalOpers Mask = 'o' + LocalQuits Mask = 'q' + Stats Mask = 't' + LocalAccounts Mask = 'u' + LocalXline Mask = 'x' +) + +var ( + // NoticeMaskNames has readable names for our snomask types. + NoticeMaskNames = map[Mask]string{ + LocalAccouncements: "ANNOUNCEMENT", + LocalConnects: "CONNECT", + LocalChannels: "CHANNEL", + LocalKills: "KILL", + LocalNicks: "NICK", + LocalOpers: "OPER", + LocalQuits: "QUIT", + Stats: "STATS", + LocalAccounts: "ACCOUNT", + LocalXline: "XLINE", + } +) diff --git a/irc/snomanager.go b/irc/snomanager.go new file mode 100644 index 00000000..9280ff15 --- /dev/null +++ b/irc/snomanager.go @@ -0,0 +1,117 @@ +package irc + +import ( + "fmt" + "sync" + + "github.com/DanielOaks/girc-go/ircfmt" + "github.com/DanielOaks/oragono/irc/sno" +) + +// SnoManager keeps track of which clients to send snomasks to. +type SnoManager struct { + sendListMutex sync.RWMutex + sendLists map[sno.Mask]map[*Client]bool +} + +// NewSnoManager returns a new SnoManager +func NewSnoManager() *SnoManager { + var m SnoManager + m.sendLists = make(map[sno.Mask]map[*Client]bool) + return &m +} + +// AddMasks adds the given snomasks to the client. +func (m *SnoManager) AddMasks(client *Client, masks ...sno.Mask) { + m.sendListMutex.Lock() + defer m.sendListMutex.Unlock() + + for _, mask := range masks { + currentClientList := m.sendLists[mask] + + if currentClientList == nil { + currentClientList = map[*Client]bool{} + } + + currentClientList[client] = true + + m.sendLists[mask] = currentClientList + } +} + +// RemoveMasks removes the given snomasks from the client. +func (m *SnoManager) RemoveMasks(client *Client, masks ...sno.Mask) { + m.sendListMutex.Lock() + defer m.sendListMutex.Unlock() + + for _, mask := range masks { + currentClientList := m.sendLists[mask] + + if currentClientList == nil || len(currentClientList) == 0 { + continue + } + + delete(currentClientList, client) + + m.sendLists[mask] = currentClientList + } +} + +// RemoveClient removes the given client from all of our lists. +func (m *SnoManager) RemoveClient(client *Client) { + m.sendListMutex.Lock() + defer m.sendListMutex.Unlock() + + for mask := range m.sendLists { + currentClientList := m.sendLists[mask] + + if currentClientList == nil || len(currentClientList) == 0 { + continue + } + + delete(currentClientList, client) + + m.sendLists[mask] = currentClientList + } +} + +// Send sends the given snomask to all users signed up for it. +func (m *SnoManager) Send(mask sno.Mask, content string) { + m.sendListMutex.RLock() + defer m.sendListMutex.RUnlock() + + currentClientList := m.sendLists[mask] + + if currentClientList == nil || len(currentClientList) == 0 { + return + } + + // make the message + name := sno.NoticeMaskNames[mask] + if name == "" { + name = string(mask) + } + message := fmt.Sprintf(ircfmt.Unescape("$c[grey]-$r%s$c[grey]-$c %s"), name, content) + + // send it out + for client := range currentClientList { + client.Notice(message) + } +} + +// String returns the snomasks currently enabled. +func (m *SnoManager) String(client *Client) string { + 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) + break + } + } + } + return masks +} diff --git a/oragono.yaml b/oragono.yaml index 15c5aebd..bfb961a8 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -196,6 +196,9 @@ opers: # custom hostname vhost: "n" + # modes are the modes to auto-set upon opering-up + modes: +is acjknoqtux + # password to login with /OPER command # generated using "oragono genpasswd" password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu