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.
This commit is contained in:
Daniel Oaks 2017-03-11 22:01:40 +10:00
parent 439331cfb8
commit b33b217fab
10 changed files with 343 additions and 31 deletions

View File

@ -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")

View File

@ -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.

View File

@ -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] {

90
irc/channelreg.go Normal file
View File

@ -0,0 +1,90 @@
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
// 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
}

119
irc/chanserv.go Normal file
View File

@ -0,0 +1,119 @@
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
// 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 <channel>")
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")
}
}

View File

@ -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,

View File

@ -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 <subcommand> [params]
ChanServ controls channel registrations.`,
},
"cs": {
text: `CS <subcommand> [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 <newnick>
Sets your nickname to the new given one.`,
},
"nickserv": {
text: `NICKSERV <subcommand> [params]
NickServ controls accounts and user registrations.`,
},
"notice": {
text: `NOTICE <target>{,<target>} <text to be sent>
@ -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 <subcommand> [params]
NickServ controls accounts and user registrations.`,
},
"oper": {
text: `OPER <name> <password>

View File

@ -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 <nickname>
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
}

24
irc/nickserv.go Normal file
View File

@ -0,0 +1,24 @@
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
// 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!")
}

View File

@ -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 {