this repo has no description
1package main
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "log/slog"
8 "net/http"
9 "os"
10 "os/signal"
11 "syscall"
12 "time"
13
14 "github.com/labstack/echo/v4"
15 "github.com/labstack/echo/v4/middleware"
16 slogecho "github.com/samber/slog-echo"
17)
18
19type Server struct {
20 echo *echo.Echo
21 httpd *http.Server
22 logger *slog.Logger
23}
24
25type Config struct {
26 Logger *slog.Logger
27 Bind string
28}
29
30func NewServer(config Config) (*Server, error) {
31 logger := config.Logger
32 if logger == nil {
33 logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
34 Level: slog.LevelInfo,
35 }))
36 }
37
38 e := echo.New()
39
40 // httpd
41 var (
42 httpTimeout = 1 * time.Minute
43 httpMaxHeaderBytes = 1 * (1024 * 1024)
44 )
45
46 srv := &Server{
47 echo: e,
48 logger: logger,
49 }
50
51 srv.httpd = &http.Server{
52 Handler: srv,
53 Addr: config.Bind,
54 WriteTimeout: httpTimeout,
55 ReadTimeout: httpTimeout,
56 MaxHeaderBytes: httpMaxHeaderBytes,
57 }
58
59 e.HideBanner = true
60 e.Use(slogecho.New(logger))
61 e.Use(middleware.Recover())
62 e.Use(middleware.BodyLimit("4M"))
63 e.HTTPErrorHandler = srv.errorHandler
64 e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
65 ContentTypeNosniff: "nosniff",
66 XFrameOptions: "SAMEORIGIN",
67 HSTSMaxAge: 31536000, // 365 days
68 // TODO:
69 // ContentSecurityPolicy
70 // XSSProtection
71 }))
72
73 e.GET("/", srv.WebHome)
74 e.GET("/_health", srv.HandleHealthCheck)
75
76 // XXX
77 return srv, nil
78}
79
80func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
81 srv.echo.ServeHTTP(rw, req)
82}
83
84func (srv *Server) Run() error {
85 srv.logger.Info("starting server", "bind", srv.httpd.Addr)
86 go func() {
87 if err := srv.httpd.ListenAndServe(); err != nil {
88 if !errors.Is(err, http.ErrServerClosed) {
89 srv.logger.Error("HTTP server shutting down unexpectedly", "err", err)
90 }
91 }
92 }()
93
94 // Wait for a signal to exit.
95 srv.logger.Info("registering OS exit signal handler")
96 quit := make(chan struct{})
97 exitSignals := make(chan os.Signal, 1)
98 signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM)
99 go func() {
100 sig := <-exitSignals
101 srv.logger.Info("received OS exit signal", "signal", sig)
102
103 // Shut down the HTTP server
104 if err := srv.Shutdown(); err != nil {
105 srv.logger.Error("HTTP server shutdown error", "err", err)
106 }
107
108 // Trigger the return that causes an exit.
109 close(quit)
110 }()
111 <-quit
112 srv.logger.Info("graceful shutdown complete")
113 return nil
114}
115
116func (srv *Server) Shutdown() error {
117 srv.logger.Info("shutting down")
118
119 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
120 defer cancel()
121
122 return srv.httpd.Shutdown(ctx)
123}
124
125type GenericError struct {
126 Error string `json:"error"`
127 Message string `json:"message"`
128}
129
130func (srv *Server) errorHandler(err error, c echo.Context) {
131 code := http.StatusInternalServerError
132 var errorMessage string
133 if he, ok := err.(*echo.HTTPError); ok {
134 code = he.Code
135 errorMessage = fmt.Sprintf("%s", he.Message)
136 }
137 if code >= 500 {
138 srv.logger.Warn("atbin-http-internal-error", "err", err)
139 }
140 if !c.Response().Committed {
141 c.JSON(code, GenericError{Error: "InternalError", Message: errorMessage})
142 }
143}