aboutsummaryrefslogtreecommitdiff
path: root/echo/echo_default.go
diff options
context:
space:
mode:
Diffstat (limited to 'echo/echo_default.go')
-rw-r--r--echo/echo_default.go177
1 files changed, 122 insertions, 55 deletions
diff --git a/echo/echo_default.go b/echo/echo_default.go
index ddaab44..92a5cfd 100644
--- a/echo/echo_default.go
+++ b/echo/echo_default.go
@@ -6,13 +6,17 @@ import (
6 "fmt" 6 "fmt"
7 "html/template" 7 "html/template"
8 "io/fs" 8 "io/fs"
9 "net"
9 "net/http" 10 "net/http"
10 "path" 11 "path"
12 "strconv"
11 "sync" 13 "sync"
12 14
13 gltls "code.crute.us/mcrute/golib/crypto/tls" 15 gltls "code.crute.us/mcrute/golib/crypto/tls"
14 glmw "code.crute.us/mcrute/golib/echo/middleware" 16 glmw "code.crute.us/mcrute/golib/echo/middleware"
15 "code.crute.us/mcrute/golib/echo/prometheus" 17 "code.crute.us/mcrute/golib/echo/prometheus"
18 glnet "code.crute.us/mcrute/golib/net"
19 glservice "code.crute.us/mcrute/golib/service"
16 20
17 "github.com/labstack/echo/v4" 21 "github.com/labstack/echo/v4"
18 "github.com/labstack/echo/v4/middleware" 22 "github.com/labstack/echo/v4/middleware"
@@ -22,21 +26,23 @@ import (
22// Docs: https://echo.labstack.com/guide/ 26// Docs: https://echo.labstack.com/guide/
23 27
24// TODO: 28// TODO:
25// - Intgrate prometheus
26// - Integrate CORS
27// - Access control prometheus by IP
28// - Fix redirectors to consider port
29// - Fix redirectors to not redirect IP addresses
30// - Integrate CSRF 29// - Integrate CSRF
31// - Integrate session 30// - Integrate session
32// - Use bodylimit e.Use(middleware.BodyLimit("2M")) 31// - Enable auto cert management by passing hostnames
32
33const (
34 defaultBodySizeLimit = "10M"
35)
33 36
34type EchoConfig struct { 37type EchoConfig struct {
35 Debug bool 38 Debug bool
36 BindAddress string 39 Hostnames []string
37 BindTLSAddress string 40 BindAddresses []string
41 BindTLSAddresses []string
38 TLSCacheDir string 42 TLSCacheDir string
43 BodySizeLimit string
39 TrustedProxyIPRanges []string 44 TrustedProxyIPRanges []string
45 ManagementIPRanges []string
40 EmbeddedTemplates fs.FS 46 EmbeddedTemplates fs.FS
41 DiskTemplates fs.FS 47 DiskTemplates fs.FS
42 TemplateGlob *string 48 TemplateGlob *string
@@ -44,14 +50,16 @@ type EchoConfig struct {
44 CombinedHostLogFile string 50 CombinedHostLogFile string
45 RedirectToWWW bool 51 RedirectToWWW bool
46 ContentSecurityPolicy *glmw.ContentSecurityPolicyConfig 52 ContentSecurityPolicy *glmw.ContentSecurityPolicyConfig
53 DisablePrometheus bool
47 PrometheusConfig *prometheus.PrometheusConfig 54 PrometheusConfig *prometheus.PrometheusConfig
55 CORSConfig *middleware.CORSConfig
48} 56}
49 57
50type EchoWrapper struct { 58type EchoWrapper struct {
51 *echo.Echo 59 *echo.Echo
52 tlsServer http.Server 60 servers []*http.Server
61 tlsServers []*http.Server
53 templateFS fs.FS 62 templateFS fs.FS
54 bindAddress string
55 ocspErrors chan gltls.OcspError 63 ocspErrors chan gltls.OcspError
56 ocspManager *gltls.OcspManager 64 ocspManager *gltls.OcspManager
57 initDone bool 65 initDone bool
@@ -79,45 +87,56 @@ func (w *EchoWrapper) RunCertificateManager(ctx context.Context, wg *sync.WaitGr
79 return w.ocspManager.Run(ctx, wg) 87 return w.ocspManager.Run(ctx, wg)
80} 88}
81 89
82func (w *EchoWrapper) run(ctx context.Context, wg *sync.WaitGroup, f func() error, sf func(context.Context) error) error { 90func (w *EchoWrapper) makeServerJob(s *http.Server, echoInit bool) glservice.RunnerFunc {
83 if !w.initDone { 91 return func(ctx context.Context, wg *sync.WaitGroup) error {
84 return fmt.Errorf("Echo is not initialized. Call Init()") 92 if !w.initDone {
85 } 93 return fmt.Errorf("Echo is not initialized. Call Init()")
94 }
86 95
87 wg.Add(1) 96 wg.Add(1)
88 defer wg.Done() 97 defer wg.Done()
89 98
90 err := make(chan error) 99 w.Logger.Infof("Starting server with address: %s", s.Addr)
91 go func() { err <- f() }()
92 select {
93 case e := <-err:
94 return e
95 default:
96 }
97 100
98 select { 101 err := make(chan error)
99 case <-ctx.Done(): 102 go func() {
100 w.Logger.Info("Shutting down web server") 103 if s.TLSConfig == nil && echoInit {
101 return sf(ctx) 104 err <- w.Echo.StartServer(s)
105 } else if s.TLSConfig == nil && !echoInit {
106 err <- s.ListenAndServe()
107 } else {
108 err <- s.ListenAndServeTLS("", "")
109 }
110 }()
111 select {
112 case e := <-err:
113 return e
114 default:
115 }
116
117 select {
118 case <-ctx.Done():
119 w.Logger.Info("Shutting down web server")
120 return s.Shutdown(ctx)
121 }
102 } 122 }
103} 123}
104 124
105func (w *EchoWrapper) Serve(ctx context.Context, wg *sync.WaitGroup) error { 125func (w *EchoWrapper) MakeServerJobs() []glservice.RunnerFunc {
106 return w.run( 126 out := []glservice.RunnerFunc{}
107 ctx,
108 wg,
109 func() error { return w.Echo.Start(w.bindAddress) },
110 func(ctx context.Context) error { return w.Echo.Shutdown(ctx) },
111 )
112}
113 127
114func (w *EchoWrapper) ServeTLS(ctx context.Context, wg *sync.WaitGroup) error { 128 for i, s := range w.servers {
115 return w.run( 129 // The first http (not https) server should do an echo.StartServer to
116 ctx, 130 // configure some internal echo state and print the banner (if
117 wg, 131 // configured).
118 func() error { return w.tlsServer.ListenAndServeTLS("", "") }, 132 out = append(out, w.makeServerJob(s, i == 0))
119 func(ctx context.Context) error { return w.tlsServer.Shutdown(ctx) }, 133 }
120 ) 134
135 for _, s := range w.tlsServers {
136 out = append(out, w.makeServerJob(s, false))
137 }
138
139 return out
121} 140}
122 141
123func (w *EchoWrapper) GetTemplateFS() fs.FS { 142func (w *EchoWrapper) GetTemplateFS() fs.FS {
@@ -183,25 +202,55 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
183 Errors: cmek, 202 Errors: cmek,
184 } 203 }
185 204
186 ts := http.Server{ 205 servers := make([]*http.Server, len(c.BindAddresses))
187 Addr: c.BindTLSAddress, 206 for i, a := range c.BindAddresses {
188 TLSConfig: &tls.Config{ 207 servers[i] = &http.Server{
189 MinVersion: tls.VersionTLS12, 208 Addr: a,
190 GetCertificate: cm.GetCertificate, 209 Handler: e,
191 }, 210 }
192 Handler: e, 211 }
212
213 tlsServers := make([]*http.Server, len(c.BindTLSAddresses))
214 for i, a := range c.BindTLSAddresses {
215 tlsServers[i] = &http.Server{
216 Addr: a,
217 TLSConfig: &tls.Config{
218 MinVersion: tls.VersionTLS12,
219 GetCertificate: cm.GetCertificate,
220 },
221 Handler: e,
222 }
193 } 223 }
194 224
195 e.Use(middleware.Logger()) 225 e.Use(middleware.Logger())
196 e.Use(glmw.Recover()) 226 e.Use(glmw.Recover())
197 e.Use(middleware.HTTPSRedirect()) 227
228 if c.BodySizeLimit == "" {
229 e.Use(middleware.BodyLimit(defaultBodySizeLimit))
230 } else if c.BodySizeLimit != "0" {
231 e.Use(middleware.BodyLimit(c.BodySizeLimit))
232 }
233
234 _, tlsPort, err := net.SplitHostPort(tlsServers[0].Addr)
235 if err != nil {
236 return nil, fmt.Errorf("Unable to split TLS addr and port: %w", err)
237 }
238 tlsPortI, err := strconv.Atoi(tlsPort)
239 if err != nil {
240 return nil, fmt.Errorf("Unable to convert TLS port to int: %w", err)
241 }
242 e.Use(glmw.HTTPSRedirectWithConfig(glmw.HTTPSRedirectConfig{Port: tlsPortI}))
243
198 if c.RedirectToWWW { 244 if c.RedirectToWWW {
199 e.Use(middleware.WWWRedirect()) 245 e.Use(middleware.WWWRedirect())
200 } 246 }
247
201 e.Use(middleware.Decompress()) 248 e.Use(middleware.Decompress())
202 e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 249 e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
203 // TODO: This mw causes prometheus responses to show up compressed for 250 // TODO: This mw causes prometheus responses to show up compressed for
204 // browsers. Why? 251 // browsers. Why?
252 //
253 // Also, this path should use the config path if we keep it
205 Skipper: func(c echo.Context) bool { 254 Skipper: func(c echo.Context) bool {
206 if c.Path() == "/metrics" { 255 if c.Path() == "/metrics" {
207 return true 256 return true
@@ -210,24 +259,42 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
210 }, 259 },
211 Level: 5, 260 Level: 5,
212 })) 261 }))
262
213 e.Use(glmw.StrictSecure()) 263 e.Use(glmw.StrictSecure())
214 264
265 if c.CORSConfig != nil {
266 e.Use(middleware.CORSWithConfig(*c.CORSConfig))
267 } else {
268 e.Use(middleware.CORS())
269 }
270
215 if c.ContentSecurityPolicy != nil { 271 if c.ContentSecurityPolicy != nil {
216 e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy)) 272 e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy))
217 } else { 273 } else {
218 return nil, fmt.Errorf("ContentSecurityPolicy is required") 274 return nil, fmt.Errorf("ContentSecurityPolicy is required")
219 } 275 }
220 276
221 if c.PrometheusConfig != nil { 277 if !c.DisablePrometheus {
222 prom := prometheus.NewPrometheusWithConfig(c.PrometheusConfig) 278 mips, err := glnet.ParseCIDRSlice(c.ManagementIPRanges)
279 if err != nil {
280 return nil, err
281 }
282
283 var prom *prometheus.Prometheus
284 if c.PrometheusConfig != nil {
285 prom = prometheus.NewPrometheusWithConfig(c.PrometheusConfig)
286 } else {
287 prom = prometheus.NewPrometheus()
288 }
289
223 e.Use(prom.MiddlewareHandler) 290 e.Use(prom.MiddlewareHandler)
224 e.GET(c.PrometheusConfig.MetricsPath, prom.MetricsHandler) 291 e.GET(prom.Config.MetricsPath, prom.MetricsHandler, glmw.NewIPFilter(mips))
225 } 292 }
226 293
227 return &EchoWrapper{ 294 return &EchoWrapper{
228 Echo: e, 295 Echo: e,
229 tlsServer: ts, 296 servers: servers,
230 bindAddress: c.BindAddress, 297 tlsServers: tlsServers,
231 ocspErrors: cmek, 298 ocspErrors: cmek,
232 ocspManager: cm, 299 ocspManager: cm,
233 templateFS: templates, 300 templateFS: templates,