From 960119eaaf74a6ab34961254f98ba8de735da3c0 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Fri, 7 Apr 2017 00:06:12 -0400 Subject: [PATCH] Support reloading the TLS config via SIGHUP. - Breaks out running an Accept loop into a goroutine. - Changes how the servers communicate back to the main event loop, using an enum instead of an error object. --- README.md | 5 ++++ mailpopbox.go | 17 ++++++++++---- pop3.go | 65 +++++++++++++++++++++++++++++++++------------------ server.go | 36 ++++++++++++++++++++++++++++ smtp.go | 54 ++++++++++++++++++++++++++++-------------- 5 files changed, 131 insertions(+), 46 deletions(-) create mode 100644 server.go diff --git a/README.md b/README.md index 827db6b..e424860 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ Mailpopbox is a combination delivery SMTP server and POP mailbox. The purpose is catch-all delivery server for an MX domain. All messages that it receives are deposited into a single mailbox, which can then be accessed using the POP3 protocol. +## TLS Support + +TLS is recommended in production environments. To facilitate live-reloading of certificates, you can +send a running instance SIGHUP. + ## RFCs This server implements the following RFCs: diff --git a/mailpopbox.go b/mailpopbox.go index ece1e6b..2966d4f 100644 --- a/mailpopbox.go +++ b/mailpopbox.go @@ -32,10 +32,17 @@ func main() { pop3 := runPOP3Server(config, log) smtp := runSMTPServer(config, log) - select { - case err := <-pop3: - fmt.Println(err) - case err := <-smtp: - fmt.Println(err) + for { + select { + case cm := <-pop3: + if cm == ServerControlRestart { + pop3 = runPOP3Server(config, log) + } else { + break + } + case <-smtp: + // smtp never reloads. + break + } } } diff --git a/pop3.go b/pop3.go index 53c4ebc..01b753c 100644 --- a/pop3.go +++ b/pop3.go @@ -15,35 +15,64 @@ import ( "src.bluestatic.org/mailpopbox/pop3" ) -func runPOP3Server(config Config, log zap.Logger) <-chan error { +func runPOP3Server(config Config, log zap.Logger) <-chan ServerControlMessage { server := pop3Server{ - config: config, - rc: make(chan error), - log: log.With(zap.String("server", "pop3")), + config: config, + controlChan: make(chan ServerControlMessage), + log: log.With(zap.String("server", "pop3")), } go server.run() - return server.rc + return server.controlChan } type pop3Server struct { - config Config - rc chan error - log zap.Logger + config Config + controlChan chan ServerControlMessage + log zap.Logger } func (server *pop3Server) run() { for _, s := range server.config.Servers { if err := os.Mkdir(s.MaildropPath, 0700); err != nil && !os.IsExist(err) { server.log.Error("failed to open maildrop", zap.Error(err)) - server.rc <- err + server.controlChan <- ServerControlFatalError } } + l, err := server.newListener() + if err != nil { + server.controlChan <- ServerControlFatalError + return + } + + connChan := make(chan net.Conn) + go RunAcceptLoop(l, connChan, server.log) + + reloadChan := CreateReloadSignal() + + for { + select { + case <-reloadChan: + server.log.Info("restarting server") + l.Close() + server.controlChan <- ServerControlRestart + break + case conn, ok := <-connChan: + if ok { + go pop3.AcceptConnection(conn, server, server.log) + } else { + server.controlChan <- ServerControlFatalError + break + } + } + } +} + +func (server *pop3Server) newListener() (net.Listener, error) { tlsConfig, err := server.config.GetTLSConfig() if err != nil { server.log.Error("failed to configure TLS", zap.Error(err)) - server.rc <- err - return + return nil, err } addr := fmt.Sprintf(":%d", server.config.POP3Port) @@ -57,20 +86,10 @@ func (server *pop3Server) run() { } if err != nil { server.log.Error("listen", zap.Error(err)) - server.rc <- err - return + return nil, err } - for { - conn, err := l.Accept() - if err != nil { - server.log.Error("accept", zap.Error(err)) - server.rc <- err - break - } - - go pop3.AcceptConnection(conn, server, server.log) - } + return l, nil } func (server *pop3Server) Name() string { diff --git a/server.go b/server.go new file mode 100644 index 0000000..04990d8 --- /dev/null +++ b/server.go @@ -0,0 +1,36 @@ +package main + +import ( + "net" + "os" + "os/signal" + "syscall" + + "github.com/uber-go/zap" +) + +type ServerControlMessage int + +const ( + ServerControlFatalError ServerControlMessage = iota + ServerControlRestart +) + +func RunAcceptLoop(l net.Listener, c chan<- net.Conn, log zap.Logger) { + for { + conn, err := l.Accept() + if err != nil { + log.Error("accept", zap.Error(err)) + close(c) + return + } + + c <- conn + } +} + +func CreateReloadSignal() <-chan os.Signal { + reloadChan := make(chan os.Signal, 1) + signal.Notify(reloadChan, syscall.SIGHUP) + return reloadChan +} diff --git a/smtp.go b/smtp.go index a4d1c19..ef71dab 100644 --- a/smtp.go +++ b/smtp.go @@ -14,14 +14,14 @@ import ( "src.bluestatic.org/mailpopbox/smtp" ) -func runSMTPServer(config Config, log zap.Logger) <-chan error { +func runSMTPServer(config Config, log zap.Logger) <-chan ServerControlMessage { server := smtpServer{ - config: config, - rc: make(chan error), - log: log.With(zap.String("server", "smtp")), + config: config, + controlChan: make(chan ServerControlMessage), + log: log.With(zap.String("server", "smtp")), } go server.run() - return server.rc + return server.controlChan } type smtpServer struct { @@ -30,15 +30,12 @@ type smtpServer struct { log zap.Logger - rc chan error + controlChan chan ServerControlMessage } func (server *smtpServer) run() { - var err error - server.tlsConfig, err = server.config.GetTLSConfig() - if err != nil { - server.log.Error("failed to configure TLS", zap.Error(err)) - server.rc <- err + if !server.loadTLSConfig() { + return } addr := fmt.Sprintf(":%d", server.config.SMTPPort) @@ -47,20 +44,41 @@ func (server *smtpServer) run() { l, err := net.Listen("tcp", addr) if err != nil { server.log.Error("listen", zap.Error(err)) - server.rc <- err + server.controlChan <- ServerControlFatalError return } + connChan := make(chan net.Conn) + go RunAcceptLoop(l, connChan, server.log) + + reloadChan := CreateReloadSignal() + for { - conn, err := l.Accept() - if err != nil { - server.log.Error("accept", zap.Error(err)) - server.rc <- err - return + select { + case <-reloadChan: + if !server.loadTLSConfig() { + return + } + case conn, ok := <-connChan: + if ok { + go smtp.AcceptConnection(conn, server, server.log) + } else { + break + } } + } +} - go smtp.AcceptConnection(conn, server, server.log) +func (server *smtpServer) loadTLSConfig() bool { + var err error + server.tlsConfig, err = server.config.GetTLSConfig() + if err != nil { + server.log.Error("failed to configure TLS", zap.Error(err)) + server.controlChan <- ServerControlFatalError + return false } + server.log.Info("loaded TLS config") + return true } func (server *smtpServer) Name() string { -- 2.43.5