3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-10 22:19:31 +01:00

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" "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 ( var (
errAccountCreation = errors.New("Account could not be created") errAccountCreation = errors.New("Account could not be created")
errCertfpAlreadyExists = errors.New("An account already exists with your certificate") errCertfpAlreadyExists = errors.New("An account already exists with your certificate")

View File

@ -17,6 +17,15 @@ import (
"github.com/tidwall/buntdb" "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 ( var (
// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support. // 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. // 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" "sync"
"github.com/DanielOaks/girc-go/ircmsg" "github.com/DanielOaks/girc-go/ircmsg"
"github.com/tidwall/buntdb"
) )
type Channel struct { type Channel struct {
@ -277,10 +278,27 @@ func (channel *Channel) Join(client *Client, key string) {
client.channels.Add(channel) client.channels.Add(channel)
channel.members.Add(client) channel.members.Add(client)
if len(channel.members) == 1 { if len(channel.members) == 1 {
channel.createdTime = time.Now() client.server.registeredChannelsMutex.Lock()
// // we should only do this on registered channels defer client.server.registeredChannelsMutex.Unlock()
// channel.members[client][ChannelFounder] = true client.server.store.Update(func(tx *buntdb.Tx) error {
channel.members[client][ChannelOperator] = true 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] { 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, usablePreReg: true,
minParams: 1, minParams: 1,
}, },
"CHANSERV": {
handler: csHandler,
minParams: 1,
},
"CS": {
handler: csHandler,
minParams: 1,
},
"DEBUG": { "DEBUG": {
handler: debugHandler, handler: debugHandler,
minParams: 1, minParams: 1,
@ -143,6 +151,10 @@ var Commands = map[string]Command{
usablePreReg: true, usablePreReg: true,
minParams: 1, minParams: 1,
}, },
"NICKSERV": {
handler: nsHandler,
minParams: 1,
},
"NOTICE": { "NOTICE": {
handler: noticeHandler, handler: noticeHandler,
minParams: 2, minParams: 2,
@ -155,6 +167,10 @@ var Commands = map[string]Command{
handler: npcaHandler, handler: npcaHandler,
minParams: 3, minParams: 3,
}, },
"NS": {
handler: nsHandler,
minParams: 1,
},
"OPER": { "OPER": {
handler: operHandler, handler: operHandler,
minParams: 2, minParams: 2,

View File

@ -84,6 +84,16 @@ longer away.`,
Used in capability negotiation. See the IRCv3 specs for more info: 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.1.html
http://ircv3.net/specs/core/capability-negotiation-3.2.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": { "debug": {
oper: true, oper: true,
@ -239,6 +249,11 @@ view the channel membership prefixes supported by this server, see the help for
text: `NICK <newnick> text: `NICK <newnick>
Sets your nickname to the new given one.`, Sets your nickname to the new given one.`,
},
"nickserv": {
text: `NICKSERV <subcommand> [params]
NickServ controls accounts and user registrations.`,
}, },
"notice": { "notice": {
text: `NOTICE <target>{,<target>} <text to be sent> 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. 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.`, Requires the roleplay mode (+E) to be set on the target.`,
},
"ns": {
text: `NS <subcommand> [params]
NickServ controls accounts and user registrations.`,
}, },
"oper": { "oper": {
text: `OPER <name> <password> text: `OPER <name> <password>

View File

@ -11,6 +11,14 @@ import (
"github.com/DanielOaks/girc-go/ircmsg" "github.com/DanielOaks/girc-go/ircmsg"
) )
var (
restrictedNicknames = map[string]bool{
"=scene=": true, // used for rp commands
"chanserv": true,
"nickserv": true,
}
)
// NICK <nickname> // NICK <nickname>
func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
if !client.authorized { if !client.authorized {
@ -26,7 +34,7 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
return false 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") client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, nicknameRaw, "Erroneous nickname")
return false return false
} }
@ -70,7 +78,7 @@ func sanickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
return false 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") client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, msg.Params[0], "Erroneous nickname")
return false 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 operclasses map[string]OperClass
password []byte password []byte
passwords *PasswordManager passwords *PasswordManager
registeredChannels map[string]*RegisteredChannel
registeredChannelsMutex sync.RWMutex
rehashMutex sync.Mutex rehashMutex sync.Mutex
rehashSignal chan os.Signal rehashSignal chan os.Signal
restAPI *RestAPIConfig restAPI *RestAPIConfig
@ -185,9 +187,10 @@ func NewServer(configFilename string, config *Config, logger *logger.Manager) (*
} }
server := &Server{ server := &Server{
accounts: make(map[string]*ClientAccount),
accountAuthenticationEnabled: config.Accounts.AuthenticationEnabled, accountAuthenticationEnabled: config.Accounts.AuthenticationEnabled,
accounts: make(map[string]*ClientAccount),
channels: make(ChannelNameMap), channels: make(ChannelNameMap),
checkIdent: config.Server.CheckIdent,
clients: NewClientLookupSet(), clients: NewClientLookupSet(),
commands: make(chan Command), commands: make(chan Command),
configFilename: configFilename, configFilename: configFilename,
@ -209,21 +212,21 @@ func NewServer(configFilename string, config *Config, logger *logger.Manager) (*
Rest: config.Limits.LineLen.Rest, Rest: config.Limits.LineLen.Rest,
}, },
}, },
listeners: make(map[string]ListenerInterface), listeners: make(map[string]ListenerInterface),
logger: logger, logger: logger,
monitoring: make(map[string][]Client), monitoring: make(map[string][]Client),
name: config.Server.Name, name: config.Server.Name,
nameCasefolded: casefoldedName, nameCasefolded: casefoldedName,
networkName: config.Network.Name, networkName: config.Network.Name,
newConns: make(chan clientConn), newConns: make(chan clientConn),
operclasses: *operClasses, operators: opers,
operators: opers, operclasses: *operClasses,
signals: make(chan os.Signal, len(ServerExitSignals)), registeredChannels: make(map[string]*RegisteredChannel),
stsEnabled: config.Server.STS.Enabled, rehashSignal: make(chan os.Signal, 1),
rehashSignal: make(chan os.Signal, 1), restAPI: &config.Server.RestAPI,
restAPI: &config.Server.RestAPI, signals: make(chan os.Signal, len(ServerExitSignals)),
whoWas: NewWhoWasList(config.Limits.WhowasEntries), stsEnabled: config.Server.STS.Enabled,
checkIdent: config.Server.CheckIdent, whoWas: NewWhoWasList(config.Limits.WhowasEntries),
} }
// open data store // open data store
@ -949,6 +952,13 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool
channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg) channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg)
} else { } else {
target, err = CasefoldName(targetString) 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) user := server.clients.Get(target)
if err != nil || user == nil { if err != nil || user == nil {
if len(target) > 0 { if len(target) > 0 {
@ -1593,6 +1603,13 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
if err != nil { if err != nil {
continue continue
} }
if target == "chanserv" {
server.chanservReceiveNotice(client, message)
continue
} else if target == "nickserv" {
server.nickservReceiveNotice(client, message)
continue
}
user := server.clients.Get(target) user := server.clients.Get(target)
if user == nil { if user == nil {