package server import ( "context" "crypto/tls" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-co-op/gocron/v2" ) // Server represents an HTTP server. type Server struct { srv *http.Server logger *slog.Logger cron gocron.Scheduler cancel context.CancelFunc startErr chan error disableTls bool tlsCertFilename string tlsKeyFilename string encryptionKey string } // New creates a new server with the given logger, address and options. func New(logger *slog.Logger, cron gocron.Scheduler, cancel context.CancelFunc, addr string, opts ...Option) *Server { // Set some options for TLS tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, } srv := &http.Server{ Addr: addr, WriteTimeout: 2 * time.Minute, ReadTimeout: 30 * time.Second, ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, TLSConfig: tlsConfig, TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), } // Set the initial server values server := &Server{ srv: srv, logger: logger, cron: cron, cancel: cancel, startErr: make(chan error, 1), } // Apply any options for _, opt := range opts { opt(server) } return server } // Option represents a server option. type Option func(*Server) // WithWriteTimeout sets the write timeout. func WithWriteTimeout(timeout time.Duration) Option { return func(s *Server) { s.srv.WriteTimeout = timeout } } // WithReadTimeout sets the read timeout. func WithReadTimeout(timeout time.Duration) Option { return func(s *Server) { s.srv.ReadTimeout = timeout } } // WithRouter sets the handler. func WithRouter(handler http.Handler) Option { return func(s *Server) { s.srv.Handler = handler } } // SetKey sets the encryption key we use when generating secrets func SetKey(key string) Option { return func(s *Server) { s.encryptionKey = key } } // SetTls sets the disable tls value func SetTls(disableTls bool) Option { return func(s *Server) { s.disableTls = disableTls } } // SetCertificate sets the path to the certificate used for TLS, in PEM format func SetCertificate(tlsCertFilename string) Option { return func(s *Server) { //fmt.Printf("Setting tlsCertFilename to '%s'\n", tlsCertFilename) s.tlsCertFilename = tlsCertFilename } } // SetPrivateKey sets the path to the private key used for TLS, in PEM format func SetPrivateKey(tlsKeyFilename string) Option { return func(s *Server) { s.tlsKeyFilename = tlsKeyFilename } } // StartAndWait starts the server and waits for a signal to shut down. func (s *Server) StartAndWait() error { s.Start() return s.GracefulShutdown() } // Start starts the server. func (s *Server) Start() { go func() { if s.disableTls { s.logger.Info("starting server", "port", s.srv.Addr) if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { s.startErr <- err } } else { s.logger.Info("starting TLS server", "port", s.srv.Addr, "cert", s.tlsCertFilename, "key", s.tlsKeyFilename) if err := s.srv.ListenAndServeTLS(s.tlsCertFilename, s.tlsKeyFilename); err != nil && err != http.ErrServerClosed { s.startErr <- err } } }() } // GracefulShutdown shuts down the server gracefully. func (s *Server) GracefulShutdown() error { signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt, syscall.SIGTERM) defer signal.Stop(signals) var startErr error select { case sig := <-signals: s.logger.Info("shutdown signal received", "signal", sig.String()) // Create a deadline to wait for. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Doesn't block if no connections, but will otherwise wait // until the timeout deadline. if err := s.srv.Shutdown(ctx); err != nil { s.logger.Warn("http server shutdown returned error", "error", err) } case err := <-s.startErr: s.logger.Error("http server exited with startup/runtime error", "error", err) startErr = err } s.logger.Info("running cron shutdown") if err := s.cron.Shutdown(); err != nil { s.logger.Error("error shutting cron", "error", err) } s.logger.Info("running cancel") s.cancel() s.logger.Info("shutting down") return startErr }