From b33b217fab866340779e53d058605e3d5749c116 Mon Sep 17 00:00:00 2001 From: Daniel Oaks Date: Sat, 11 Mar 2017 22:01:40 +1000 Subject: [PATCH] Add very initial ChanServ and NickServ virtual clients As well, add channel registration and re-applying founder privs on the first client joining the channel. I'm going to re-architect our modes system to better acocunt for this sort of change. --- irc/{registration.go => accountreg.go} | 9 -- irc/accounts.go | 9 ++ irc/channel.go | 26 +++++- irc/channelreg.go | 90 +++++++++++++++++++ irc/chanserv.go | 119 +++++++++++++++++++++++++ irc/commands.go | 16 ++++ irc/help.go | 20 +++++ irc/nickname.go | 12 ++- irc/nickserv.go | 24 +++++ irc/server.go | 49 ++++++---- 10 files changed, 343 insertions(+), 31 deletions(-) rename irc/{registration.go => accountreg.go} (96%) create mode 100644 irc/channelreg.go create mode 100644 irc/chanserv.go create mode 100644 irc/nickserv.go diff --git a/irc/registration.go b/irc/accountreg.go similarity index 96% rename from irc/registration.go rename to irc/accountreg.go index 2c7138fc..aee70f10 100644 --- a/irc/registration.go +++ b/irc/accountreg.go @@ -16,15 +16,6 @@ import ( "github.com/tidwall/buntdb" ) -const ( - keyAccountExists = "account.exists %s" - keyAccountVerified = "account.verified %s" - keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped - keyAccountRegTime = "account.registered.time %s" - keyAccountCredentials = "account.credentials %s" - keyCertToAccount = "account.creds.certfp %s" -) - var ( errAccountCreation = errors.New("Account could not be created") errCertfpAlreadyExists = errors.New("An account already exists with your certificate") diff --git a/irc/accounts.go b/irc/accounts.go index 218ca8e4..00b9e9ac 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -17,6 +17,15 @@ import ( "github.com/tidwall/buntdb" ) +const ( + keyAccountExists = "account.exists %s" + keyAccountVerified = "account.verified %s" + keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped + keyAccountRegTime = "account.registered.time %s" + keyAccountCredentials = "account.credentials %s" + keyCertToAccount = "account.creds.certfp %s" +) + var ( // EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support. // This can be moved to some other data structure/place if we need to load/unload mechs later. diff --git a/irc/channel.go b/irc/channel.go index 808ef8d2..d4950e9d 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -14,6 +14,7 @@ import ( "sync" "github.com/DanielOaks/girc-go/ircmsg" + "github.com/tidwall/buntdb" ) type Channel struct { @@ -277,10 +278,27 @@ func (channel *Channel) Join(client *Client, key string) { client.channels.Add(channel) channel.members.Add(client) if len(channel.members) == 1 { - channel.createdTime = time.Now() - // // we should only do this on registered channels - // channel.members[client][ChannelFounder] = true - channel.members[client][ChannelOperator] = true + client.server.registeredChannelsMutex.Lock() + defer client.server.registeredChannelsMutex.Unlock() + client.server.store.Update(func(tx *buntdb.Tx) error { + chanReg := client.server.loadChannelNoMutex(tx, channel.nameCasefolded) + + if chanReg == nil { + channel.createdTime = time.Now() + channel.members[client][ChannelOperator] = true + } else { + // we should only do this on registered channels + if client.account != nil && client.account.Name == chanReg.Founder { + channel.members[client][ChannelFounder] = true + } + channel.topic = chanReg.Topic + channel.topicSetBy = chanReg.TopicSetBy + channel.topicSetTime = chanReg.TopicSetTime + channel.name = chanReg.Name + channel.createdTime = chanReg.RegisteredAt + } + return nil + }) } if client.capabilities[ExtendedJoin] { diff --git a/irc/channelreg.go b/irc/channelreg.go new file mode 100644 index 00000000..a004f4ef --- /dev/null +++ b/irc/channelreg.go @@ -0,0 +1,90 @@ +// Copyright (c) 2016- Daniel Oaks +// released under the MIT license + +package irc + +import ( + "errors" + "fmt" + "strconv" + "time" + + "github.com/tidwall/buntdb" +) + +const ( + keyChannelExists = "channel.exists %s" + keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped + keyChannelRegTime = "channel.registered.time %s" + keyChannelFounder = "channel.founder %s" + keyChannelTopic = "channel.topic %s" + keyChannelTopicSetBy = "channel.topic.setby %s" + keyChannelTopicSetTime = "channel.topic.settime %s" +) + +var ( + errChanExists = errors.New("Channel already exists") +) + +// RegisteredChannel holds details about a given registered channel. +type RegisteredChannel struct { + // Name of the channel. + Name string + // RegisteredAt represents the time that the channel was registered. + RegisteredAt time.Time + // Founder indicates the founder of the channel. + Founder string + // Topic represents the channel topic. + Topic string + // TopicSetBy represents the host that set the topic. + TopicSetBy string + // TopicSetTime represents the time the topic was set. + TopicSetTime time.Time +} + +// loadChannelNoMutex loads a channel from the store. +func (server *Server) loadChannelNoMutex(tx *buntdb.Tx, channelKey string) *RegisteredChannel { + // return loaded chan if it already exists + if server.registeredChannels[channelKey] != nil { + return server.registeredChannels[channelKey] + } + _, err := tx.Get(fmt.Sprintf(keyChannelExists, channelKey)) + if err == buntdb.ErrNotFound { + // chan does not already exist, return + return nil + } + + // channel exists, load it + name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey)) + regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey)) + regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) + founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey)) + topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey)) + topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey)) + topicSetTime, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey)) + topicSetTimeInt, _ := strconv.ParseInt(topicSetTime, 10, 64) + + chanInfo := RegisteredChannel{ + Name: name, + RegisteredAt: time.Unix(regTimeInt, 0), + Founder: founder, + Topic: topic, + TopicSetBy: topicSetBy, + TopicSetTime: time.Unix(topicSetTimeInt, 0), + } + server.registeredChannels[channelKey] = &chanInfo + + return &chanInfo +} + +// saveChannelNoMutex saves a channel to the store. +func (server *Server) saveChannelNoMutex(tx *buntdb.Tx, channelKey string, channelInfo RegisteredChannel) { + tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil) + tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil) + tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil) + tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil) + tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil) + tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil) + tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.Unix(), 10), nil) + server.registeredChannels[channelKey] = &channelInfo +} diff --git a/irc/chanserv.go b/irc/chanserv.go new file mode 100644 index 00000000..86885a20 --- /dev/null +++ b/irc/chanserv.go @@ -0,0 +1,119 @@ +// Copyright (c) 2017 Daniel Oaks +// released under the MIT license + +package irc + +import ( + "fmt" + "strings" + "time" + + "github.com/DanielOaks/girc-go/ircmsg" + "github.com/tidwall/buntdb" +) + +// csHandler handles the /CS and /CHANSERV commands +func csHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + server.chanservReceivePrivmsg(client, strings.Join(msg.Params, " ")) + return false +} + +func (server *Server) chanservReceiveNotice(client *Client, message string) { + // do nothing +} + +// ChanServNotice sends the client a notice from ChanServ. +func (client *Client) ChanServNotice(text string) { + client.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "NOTICE", client.nick, text) +} + +func (server *Server) chanservReceivePrivmsg(client *Client, message string) { + var params []string + for _, p := range strings.Split(message, " ") { + if len(p) > 0 { + params = append(params, p) + } + } + if len(params) < 1 { + client.ChanServNotice("You need to run a command") + //TODO(dan): dump CS help here + return + } + + command := strings.ToLower(params[0]) + server.logger.Debug("chanserv", fmt.Sprintf("Client %s ran command %s", client.nick, command)) + + if command == "register" { + if len(params) < 2 { + client.ChanServNotice("Syntax: REGISTER ") + return + } + + server.registeredChannelsMutex.Lock() + defer server.registeredChannelsMutex.Unlock() + + channelName := params[1] + channelKey, err := CasefoldChannel(channelName) + if err != nil { + client.ChanServNotice("Channel name is not valid") + return + } + + channelInfo := server.channels.Get(channelKey) + if channelInfo == nil { + client.ChanServNotice("You must be an oper on the channel to register it") + return + } + + if !channelInfo.ClientIsAtLeast(client, ChannelOperator) { + client.ChanServNotice("You must be an oper on the channel to register it") + return + } + + server.store.Update(func(tx *buntdb.Tx) error { + currentChan := server.loadChannelNoMutex(tx, channelKey) + if currentChan != nil { + client.ChanServNotice("Channel is already registered") + return nil + } + + account := client.account + if account == nil { + client.ChanServNotice("You must be logged in to register a channel") + return nil + } + + chanRegInfo := RegisteredChannel{ + Name: channelName, + RegisteredAt: time.Now(), + Founder: account.Name, + Topic: channelInfo.topic, + TopicSetBy: channelInfo.topicSetBy, + TopicSetTime: channelInfo.topicSetTime, + } + server.saveChannelNoMutex(tx, channelKey, chanRegInfo) + + client.ChanServNotice(fmt.Sprintf("Channel %s successfully registered", channelName)) + + server.logger.Info("chanserv", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName)) + + channelInfo.membersMutex.Lock() + defer channelInfo.membersMutex.Unlock() + + // give them founder privs + change := channelInfo.applyModeMemberNoMutex(client, ChannelFounder, Add, client.nickCasefolded) + if change != nil { + //TODO(dan): we should change the name of String and make it return a slice here + //TODO(dan): unify this code with code in modes.go + args := append([]string{channelName}, strings.Split(change.String(), " ")...) + for member := range channelInfo.members { + member.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "MODE", args...) + } + } + + return nil + }) + } else { + client.ChanServNotice("Sorry, I don't know that command") + } +} diff --git a/irc/commands.go b/irc/commands.go index b24b4044..b2dfd74e 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -74,6 +74,14 @@ var Commands = map[string]Command{ usablePreReg: true, minParams: 1, }, + "CHANSERV": { + handler: csHandler, + minParams: 1, + }, + "CS": { + handler: csHandler, + minParams: 1, + }, "DEBUG": { handler: debugHandler, minParams: 1, @@ -143,6 +151,10 @@ var Commands = map[string]Command{ usablePreReg: true, minParams: 1, }, + "NICKSERV": { + handler: nsHandler, + minParams: 1, + }, "NOTICE": { handler: noticeHandler, minParams: 2, @@ -155,6 +167,10 @@ var Commands = map[string]Command{ handler: npcaHandler, minParams: 3, }, + "NS": { + handler: nsHandler, + minParams: 1, + }, "OPER": { handler: operHandler, minParams: 2, diff --git a/irc/help.go b/irc/help.go index 5d0fa284..96cff6a4 100644 --- a/irc/help.go +++ b/irc/help.go @@ -84,6 +84,16 @@ longer away.`, Used in capability negotiation. See the IRCv3 specs for more info: http://ircv3.net/specs/core/capability-negotiation-3.1.html http://ircv3.net/specs/core/capability-negotiation-3.2.html`, + }, + "chanserv": { + text: `CHANSERV [params] + +ChanServ controls channel registrations.`, + }, + "cs": { + text: `CS [params] + +ChanServ controls channel registrations.`, }, "debug": { oper: true, @@ -239,6 +249,11 @@ view the channel membership prefixes supported by this server, see the help for text: `NICK Sets your nickname to the new given one.`, + }, + "nickserv": { + text: `NICKSERV [params] + +NickServ controls accounts and user registrations.`, }, "notice": { text: `NOTICE {,} @@ -258,6 +273,11 @@ Requires the roleplay mode (+E) to be set on the target.`, The NPC command is used to send an action to the target as the source. Requires the roleplay mode (+E) to be set on the target.`, + }, + "ns": { + text: `NS [params] + +NickServ controls accounts and user registrations.`, }, "oper": { text: `OPER diff --git a/irc/nickname.go b/irc/nickname.go index a32ab6a1..181b5442 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -11,6 +11,14 @@ import ( "github.com/DanielOaks/girc-go/ircmsg" ) +var ( + restrictedNicknames = map[string]bool{ + "=scene=": true, // used for rp commands + "chanserv": true, + "nickserv": true, + } +) + // NICK func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { if !client.authorized { @@ -26,7 +34,7 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { return false } - if err != nil || len(nicknameRaw) > server.limits.NickLen || nickname == "=scene=" { + if err != nil || len(nicknameRaw) > server.limits.NickLen || restrictedNicknames[nickname] { client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, nicknameRaw, "Erroneous nickname") return false } @@ -70,7 +78,7 @@ func sanickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { return false } - if oerr != nil || err != nil || len(strings.TrimSpace(msg.Params[1])) > server.limits.NickLen || nickname == "=scene=" { + if oerr != nil || err != nil || len(strings.TrimSpace(msg.Params[1])) > server.limits.NickLen || restrictedNicknames[nickname] { client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, msg.Params[0], "Erroneous nickname") return false } diff --git a/irc/nickserv.go b/irc/nickserv.go new file mode 100644 index 00000000..474c196f --- /dev/null +++ b/irc/nickserv.go @@ -0,0 +1,24 @@ +// Copyright (c) 2017 Daniel Oaks +// released under the MIT license + +package irc + +import ( + "strings" + + "github.com/DanielOaks/girc-go/ircmsg" +) + +// nsHandler handles the /NS and /NICKSERV commands +func nsHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + server.nickservReceivePrivmsg(client, strings.Join(msg.Params, " ")) + return false +} + +func (server *Server) nickservReceiveNotice(client *Client, message string) { + // do nothing +} + +func (server *Server) nickservReceivePrivmsg(client *Client, message string) { + client.Notice("NickServ is not yet implemented, sorry!") +} diff --git a/irc/server.go b/irc/server.go index 8d4466ba..5deace4e 100644 --- a/irc/server.go +++ b/irc/server.go @@ -114,6 +114,8 @@ type Server struct { operclasses map[string]OperClass password []byte passwords *PasswordManager + registeredChannels map[string]*RegisteredChannel + registeredChannelsMutex sync.RWMutex rehashMutex sync.Mutex rehashSignal chan os.Signal restAPI *RestAPIConfig @@ -185,9 +187,10 @@ func NewServer(configFilename string, config *Config, logger *logger.Manager) (* } server := &Server{ - accounts: make(map[string]*ClientAccount), accountAuthenticationEnabled: config.Accounts.AuthenticationEnabled, + accounts: make(map[string]*ClientAccount), channels: make(ChannelNameMap), + checkIdent: config.Server.CheckIdent, clients: NewClientLookupSet(), commands: make(chan Command), configFilename: configFilename, @@ -209,21 +212,21 @@ func NewServer(configFilename string, config *Config, logger *logger.Manager) (* Rest: config.Limits.LineLen.Rest, }, }, - listeners: make(map[string]ListenerInterface), - logger: logger, - monitoring: make(map[string][]Client), - name: config.Server.Name, - nameCasefolded: casefoldedName, - networkName: config.Network.Name, - newConns: make(chan clientConn), - operclasses: *operClasses, - operators: opers, - signals: make(chan os.Signal, len(ServerExitSignals)), - stsEnabled: config.Server.STS.Enabled, - rehashSignal: make(chan os.Signal, 1), - restAPI: &config.Server.RestAPI, - whoWas: NewWhoWasList(config.Limits.WhowasEntries), - checkIdent: config.Server.CheckIdent, + listeners: make(map[string]ListenerInterface), + logger: logger, + monitoring: make(map[string][]Client), + name: config.Server.Name, + nameCasefolded: casefoldedName, + networkName: config.Network.Name, + newConns: make(chan clientConn), + operators: opers, + operclasses: *operClasses, + registeredChannels: make(map[string]*RegisteredChannel), + rehashSignal: make(chan os.Signal, 1), + restAPI: &config.Server.RestAPI, + signals: make(chan os.Signal, len(ServerExitSignals)), + stsEnabled: config.Server.STS.Enabled, + whoWas: NewWhoWasList(config.Limits.WhowasEntries), } // open data store @@ -949,6 +952,13 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg) } else { target, err = CasefoldName(targetString) + if target == "chanserv" { + server.chanservReceivePrivmsg(client, message) + continue + } else if target == "nickserv" { + server.nickservReceivePrivmsg(client, message) + continue + } user := server.clients.Get(target) if err != nil || user == nil { if len(target) > 0 { @@ -1593,6 +1603,13 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { if err != nil { continue } + if target == "chanserv" { + server.chanservReceiveNotice(client, message) + continue + } else if target == "nickserv" { + server.nickservReceiveNotice(client, message) + continue + } user := server.clients.Get(target) if user == nil {