commit ea5a6aaa08ff4998be3dc8fab12d51212f3e173a
parent cf572d822632779429ea666b57c67a76c11b8b6b
Author: dwrz <dwrz@dwrz.net>
Date: Thu, 12 Jan 2023 20:32:30 +0000
Add server pkg
Diffstat:
4 files changed, 248 insertions(+), 0 deletions(-)
diff --git a/pkg/server/config.go b/pkg/server/config.go
@@ -0,0 +1,27 @@
+package server
+
+import "time"
+
+type Config struct {
+ // CertPath is the path to the x509 certificate.
+ CertPath string `json:"certPath"`
+
+ // KeyPath is the path to the x509 key.
+ KeyPath string `json:"keyPath"`
+
+ MaxHeaderBytes int `json:"maxHeaderBytes"`
+ Ports Ports `json:"ports"`
+ Timeouts Timeouts `json:"timeouts"`
+}
+
+type Ports struct {
+ HTTP string `json:"http"`
+ HTTPS string `json:"https"`
+}
+
+type Timeouts struct {
+ Idle time.Duration `json:"idle"`
+ Read time.Duration `json:"read"`
+ Shutdown time.Duration `json:"shutdown"`
+ Write time.Duration `json:"write"`
+}
diff --git a/pkg/server/new.go b/pkg/server/new.go
@@ -0,0 +1,89 @@
+package server
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net/http"
+
+ "code.dwrz.net/src/pkg/log"
+ "code.dwrz.net/src/pkg/server/redirect"
+)
+
+type Parameters struct {
+ Config Config
+ Handler http.Handler
+ Log *log.Logger
+}
+
+func (p Parameters) Validate() error {
+ if p.Handler == nil {
+ return fmt.Errorf("missing handler")
+ }
+ if p.Log == nil {
+ return fmt.Errorf("missing logger")
+ }
+
+ return nil
+}
+
+func New(p Parameters) (*Server, error) {
+ if err := p.Validate(); err != nil {
+ return nil, fmt.Errorf("invalid server parameters: %v", err)
+ }
+
+ var server = &Server{
+ cfg: &p.Config,
+ log: p.Log,
+ }
+
+ // Load TLS certificates, if provided.
+ var certs tls.Certificate
+ if p.Config.CertPath != "" && p.Config.KeyPath != "" {
+ var err error
+ certs, err = tls.LoadX509KeyPair(
+ p.Config.CertPath, p.Config.KeyPath,
+ )
+ if err != nil {
+ server.log.Error.Fatalf(
+ "failed to load x509 key pair: %v", err,
+ )
+ }
+ }
+
+ if p.Config.Ports.HTTPS != "" {
+ server.https = &http.Server{
+ Addr: fmt.Sprintf(
+ ":%s", p.Config.Ports.HTTPS,
+ ),
+ ErrorLog: p.Log.Error,
+ Handler: p.Handler,
+ ReadTimeout: p.Config.Timeouts.Read,
+ MaxHeaderBytes: p.Config.MaxHeaderBytes,
+ IdleTimeout: p.Config.Timeouts.Idle,
+ TLSConfig: &tls.Config{
+ Certificates: []tls.Certificate{certs},
+ },
+ WriteTimeout: p.Config.Timeouts.Write,
+ }
+ }
+ if p.Config.Ports.HTTP != "" {
+ server.http = &http.Server{
+ Addr: fmt.Sprintf(":%s", p.Config.Ports.HTTP),
+ ErrorLog: p.Log.Error,
+ ReadTimeout: p.Config.Timeouts.Read,
+ MaxHeaderBytes: p.Config.MaxHeaderBytes,
+ IdleTimeout: p.Config.Timeouts.Idle,
+ WriteTimeout: p.Config.Timeouts.Write,
+ }
+
+ // If serving HTTPS, redirect from HTTP.
+ // Otherwise, serve the passed-in handler over HTTP.
+ if p.Config.Ports.HTTPS != "" {
+ server.http.Handler = &redirect.Handler{}
+ } else {
+ server.http.Handler = p.Handler
+ }
+ }
+
+ return server, nil
+}
diff --git a/pkg/server/redirect/redirect.go b/pkg/server/redirect/redirect.go
@@ -0,0 +1,15 @@
+package redirect
+
+import (
+ "net/http"
+)
+
+type Handler struct{}
+
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Connection", "close")
+
+ var url = "https://" + r.Host + r.URL.String()
+
+ http.Redirect(w, r, url, http.StatusMovedPermanently)
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
@@ -0,0 +1,117 @@
+package server
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "runtime/debug"
+
+ "code.dwrz.net/src/pkg/log"
+)
+
+type Server struct {
+ cfg *Config
+ log *log.Logger
+ http *http.Server
+ https *http.Server
+}
+
+func (s *Server) Serve() {
+ if s.http != nil {
+ s.log.Debug.Printf("serving http")
+
+ go func() {
+ if msg := recover(); msg != nil {
+ s.log.Error.Printf(
+ "panic: %v:\n%s\n", msg, debug.Stack(),
+ )
+ }
+
+ s.serveHTTP()
+ }()
+ }
+ if s.https != nil {
+ s.log.Debug.Printf("serving https")
+
+ go func() {
+ if msg := recover(); msg != nil {
+ s.log.Error.Printf(
+ "panic: %v:\n%s\n", msg, debug.Stack(),
+ )
+ }
+
+ s.serveHTTPS()
+ }()
+
+ }
+
+ s.log.Debug.Printf("server ready")
+}
+
+func (s *Server) serveHTTP() {
+ s.log.Debug.Printf("starting http server (%s)", s.http.Addr)
+
+ if err := s.http.ListenAndServe(); err != http.ErrServerClosed {
+ s.log.Error.Fatalf(
+ "failed to listen and serve: %v", err,
+ )
+ }
+}
+
+func (s *Server) serveHTTPS() {
+ s.log.Debug.Printf("starting https server (%s)", s.https.Addr)
+
+ if err := s.https.ListenAndServeTLS(
+ "", "",
+ ); err != http.ErrServerClosed {
+ s.log.Error.Fatalf(
+ "failed to listen and serve: %v", err,
+ )
+ }
+}
+
+func (s *Server) Shutdown() error {
+ s.log.Debug.Printf("shutting down server")
+
+ ctxShutdown, cancel := context.WithTimeout(
+ context.Background(), s.cfg.Timeouts.Shutdown,
+ )
+ defer cancel()
+
+ var httpShutdownErr, httpsShutdownErr error
+ if s.http != nil {
+ s.http.SetKeepAlivesEnabled(false)
+
+ if err := s.http.Shutdown(ctxShutdown); err != nil {
+ httpShutdownErr = err
+ }
+ }
+ if s.https != nil {
+ s.https.SetKeepAlivesEnabled(false)
+
+ if err := s.https.Shutdown(ctxShutdown); err != nil {
+ httpsShutdownErr = err
+ }
+ }
+
+ switch {
+ case httpShutdownErr != nil && httpsShutdownErr != nil:
+ return fmt.Errorf(
+ "failed to shut down http and https servers: %v, %v",
+ httpShutdownErr, httpsShutdownErr,
+ )
+ case httpShutdownErr != nil:
+ return fmt.Errorf(
+ "failed to shut down http server: %v", httpShutdownErr,
+ )
+ case httpsShutdownErr != nil:
+ return fmt.Errorf(
+ "failed to shut down https server: %v",
+ httpsShutdownErr,
+ )
+ default:
+ s.log.Debug.Printf("server shutdown")
+
+ return nil
+ }
+}