Add bouncer MOTD

Closes: https://todo.sr.ht/~emersion/soju/137
This commit is contained in:
Simon Ser 2021-10-13 10:58:34 +02:00
parent ca9fa9198c
commit a9a066faac
6 changed files with 87 additions and 10 deletions

View File

@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
@ -36,6 +37,19 @@ func (v *stringSliceFlag) Set(s string) error {
return nil return nil
} }
func loadMOTD(srv *soju.Server, filename string) error {
if filename == "" {
return nil
}
b, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
srv.SetMOTD(strings.TrimSpace(string(b)))
return nil
}
func main() { func main() {
var listen []string var listen []string
var configPath string var configPath string
@ -91,6 +105,10 @@ func main() {
srv.MaxUserNetworks = cfg.MaxUserNetworks srv.MaxUserNetworks = cfg.MaxUserNetworks
srv.Debug = debug srv.Debug = debug
if err := loadMOTD(srv, cfg.MOTDPath); err != nil {
log.Fatalf("failed to load MOTD: %v", err)
}
for _, listen := range cfg.Listen { for _, listen := range cfg.Listen {
listenURI := listen listenURI := listen
if !strings.Contains(listenURI, ":/") { if !strings.Contains(listenURI, ":/") {
@ -224,8 +242,8 @@ func main() {
for sig := range sigCh { for sig := range sigCh {
switch sig { switch sig {
case syscall.SIGHUP: case syscall.SIGHUP:
log.Print("reloading TLS certificate and MOTD")
if cfg.TLS != nil { if cfg.TLS != nil {
log.Print("reloading TLS certificate")
cert, err := tls.LoadX509KeyPair(cfg.TLS.CertPath, cfg.TLS.KeyPath) cert, err := tls.LoadX509KeyPair(cfg.TLS.CertPath, cfg.TLS.KeyPath)
if err != nil { if err != nil {
log.Printf("failed to reload TLS certificate and key: %v", err) log.Printf("failed to reload TLS certificate and key: %v", err)
@ -233,6 +251,9 @@ func main() {
} }
tlsCert.Store(&cert) tlsCert.Store(&cert)
} }
if err := loadMOTD(srv, cfg.MOTDPath); err != nil {
log.Printf("failed to reload MOTD: %v", err)
}
case syscall.SIGINT, syscall.SIGTERM: case syscall.SIGINT, syscall.SIGTERM:
log.Print("shutting down server") log.Print("shutting down server")
srv.Shutdown() srv.Shutdown()

View File

@ -40,6 +40,7 @@ type Server struct {
Listen []string Listen []string
Hostname string Hostname string
TLS *TLS TLS *TLS
MOTDPath string
SQLDriver string SQLDriver string
SQLSource string SQLSource string
@ -128,6 +129,10 @@ func parse(cfg scfg.Block) (*Server, error) {
if srv.MaxUserNetworks, err = strconv.Atoi(max); err != nil { if srv.MaxUserNetworks, err = strconv.Atoi(max); err != nil {
return nil, fmt.Errorf("directive %q: %v", d.Name, err) return nil, fmt.Errorf("directive %q: %v", d.Name, err)
} }
case "motd":
if err := d.ParseParams(&srv.MOTDPath); err != nil {
return nil, err
}
default: default:
return nil, fmt.Errorf("unknown directive %q", d.Name) return nil, fmt.Errorf("unknown directive %q", d.Name)
} }

View File

@ -44,7 +44,8 @@ soju supports two connection modes:
For per-client history to work, clients need to indicate their name. This can For per-client history to work, clients need to indicate their name. This can
be done by adding a "@<client>" suffix to the username. be done by adding a "@<client>" suffix to the username.
soju will reload the TLS certificate and key when it receives the HUP signal. soju will reload the TLS certificate/key and the MOTD file when it receives the
HUP signal.
Administrators can broadcast a message to all bouncer users via _/notice Administrators can broadcast a message to all bouncer users via _/notice
$<hostname> <text>_, or via _/notice $\* <text>_ in multi-upstream mode. All $<hostname> <text>_, or via _/notice $\* <text>_ in multi-upstream mode. All
@ -142,6 +143,10 @@ The following directives are supported:
*max-user-networks* <limit> *max-user-networks* <limit>
Maximum number of networks per user. By default, there is no limit. Maximum number of networks per user. By default, there is no limit.
*motd* <path>
Path to the MOTD file. The bouncer MOTD is sent to clients which aren't
bound to a specific network. By default, no MOTD is sent.
# IRC SERVICE # IRC SERVICE
soju exposes an IRC service called *BouncerServ* to manage the bouncer. soju exposes an IRC service called *BouncerServ* to manage the bouncer.

View File

@ -1119,20 +1119,29 @@ func (dc *downstreamConn) welcome() error {
for _, msg := range generateIsupport(dc.srv.prefix(), dc.nick, isupport) { for _, msg := range generateIsupport(dc.srv.prefix(), dc.nick, isupport) {
dc.SendMessage(msg) dc.SendMessage(msg)
} }
motdHint := "No MOTD"
if uc := dc.upstream(); uc != nil { if uc := dc.upstream(); uc != nil {
motdHint = "Use /motd to read the message of the day"
dc.SendMessage(&irc.Message{ dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(), Prefix: dc.srv.prefix(),
Command: irc.RPL_UMODEIS, Command: irc.RPL_UMODEIS,
Params: []string{dc.nick, string(uc.modes)}, Params: []string{dc.nick, string(uc.modes)},
}) })
} }
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(), if motd := dc.user.srv.MOTD(); motd != "" && dc.network == nil {
Command: irc.ERR_NOMOTD, for _, msg := range generateMOTD(dc.srv.prefix(), dc.nick, motd) {
Params: []string{dc.nick, motdHint}, dc.SendMessage(msg)
}) }
} else {
motdHint := "No MOTD"
if dc.network != nil {
motdHint = "Use /motd to read the message of the day"
}
dc.SendMessage(&irc.Message{
Prefix: dc.srv.prefix(),
Command: irc.ERR_NOMOTD,
Params: []string{dc.nick, motdHint},
})
}
dc.updateNick() dc.updateNick()
dc.updateRealname() dc.updateRealname()

25
irc.go
View File

@ -379,6 +379,31 @@ func generateIsupport(prefix *irc.Prefix, nick string, tokens []string) []*irc.M
return msgs return msgs
} }
func generateMOTD(prefix *irc.Prefix, nick string, motd string) []*irc.Message {
var msgs []*irc.Message
msgs = append(msgs, &irc.Message{
Prefix: prefix,
Command: irc.RPL_MOTDSTART,
Params: []string{nick, fmt.Sprintf("- Message of the Day -")},
})
for _, l := range strings.Split(motd, "\n") {
msgs = append(msgs, &irc.Message{
Prefix: prefix,
Command: irc.RPL_MOTD,
Params: []string{nick, l},
})
}
msgs = append(msgs, &irc.Message{
Prefix: prefix,
Command: irc.RPL_ENDOFMOTD,
Params: []string{nick, "End of /MOTD command."},
})
return msgs
}
type joinSorter struct { type joinSorter struct {
channels []string channels []string
keys []string keys []string

View File

@ -63,10 +63,12 @@ type Server struct {
lock sync.Mutex lock sync.Mutex
listeners map[net.Listener]struct{} listeners map[net.Listener]struct{}
users map[string]*user users map[string]*user
motd atomic.Value // string
} }
func NewServer(db Database) *Server { func NewServer(db Database) *Server {
return &Server{ srv := &Server{
Logger: log.New(log.Writer(), "", log.LstdFlags), Logger: log.New(log.Writer(), "", log.LstdFlags),
HistoryLimit: 1000, HistoryLimit: 1000,
MaxUserNetworks: -1, MaxUserNetworks: -1,
@ -74,6 +76,8 @@ func NewServer(db Database) *Server {
listeners: make(map[net.Listener]struct{}), listeners: make(map[net.Listener]struct{}),
users: make(map[string]*user), users: make(map[string]*user),
} }
srv.motd.Store("")
return srv
} }
func (s *Server) prefix() *irc.Prefix { func (s *Server) prefix() *irc.Prefix {
@ -268,3 +272,11 @@ func (s *Server) Stats() *ServerStats {
stats.Downstreams = atomic.LoadInt64(&s.connCount) stats.Downstreams = atomic.LoadInt64(&s.connCount)
return &stats return &stats
} }
func (s *Server) SetMOTD(motd string) {
s.motd.Store(motd)
}
func (s *Server) MOTD() string {
return s.motd.Load().(string)
}