Support reloading the TLS config via SIGHUP. v1.2.0
authorRobert Sesek <rsesek@bluestatic.org>
Fri, 7 Apr 2017 04:06:12 +0000 (00:06 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Fri, 7 Apr 2017 04:31:26 +0000 (00:31 -0400)
- 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
mailpopbox.go
pop3.go
server.go [new file with mode: 0644]
smtp.go

index 827db6b445775e8b4a27d57701ec120212a17c6a..e424860261601dd2fefcfa06ff0259898611e493 100644 (file)
--- 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:
index ece1e6b9702e6d5d6ad825f74f938f5767d4fd89..2966d4fc619019b41ceae242f4b850108b9789e4 100644 (file)
@@ -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 53c4ebc18a6c7e3b666796605f397bb28761b216..01b753cbbcb5b157ba27c742f10e8e0cffd4adab 100644 (file)
--- 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 (file)
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 a4d1c199347df3f9b8461a2d8986dc2a960e3659..ef71dabb17459416d6394c8bb85f72993a8871f0 100644 (file)
--- 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 {