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.go180
1 files changed, 96 insertions, 84 deletions
diff --git a/echo/echo_default.go b/echo/echo_default.go
index a63f25f..e518556 100644
--- a/echo/echo_default.go
+++ b/echo/echo_default.go
@@ -6,16 +6,15 @@ import (
6 "fmt" 6 "fmt"
7 "html/template" 7 "html/template"
8 "io/fs" 8 "io/fs"
9 "net"
10 "net/http" 9 "net/http"
11 "path" 10 "os"
12 "strconv"
13 "sync" 11 "sync"
14 12
15 gltls "code.crute.us/mcrute/golib/crypto/tls" 13 glautocert "code.crute.us/mcrute/golib/crypto/acme/autocert"
16 glmw "code.crute.us/mcrute/golib/echo/middleware" 14 glmw "code.crute.us/mcrute/golib/echo/middleware"
15 "code.crute.us/mcrute/golib/echo/netbox"
17 "code.crute.us/mcrute/golib/echo/prometheus" 16 "code.crute.us/mcrute/golib/echo/prometheus"
18 glnet "code.crute.us/mcrute/golib/net" 17 "code.crute.us/mcrute/golib/service"
19 glservice "code.crute.us/mcrute/golib/service" 18 glservice "code.crute.us/mcrute/golib/service"
20 19
21 "github.com/labstack/echo/v4" 20 "github.com/labstack/echo/v4"
@@ -28,11 +27,13 @@ import (
28// TODO: 27// TODO:
29// - Integrate CSRF 28// - Integrate CSRF
30// - Integrate session 29// - Integrate session
31// - Enable auto cert management by passing hostnames
32 30
33const ( 31const defaultBodySizeLimit = "10M"
34 defaultBodySizeLimit = "10M" 32
35) 33var mandatoryTemplates = []string{
34 "404.tpl", "40x.tpl", "50x.tpl",
35 "header.tpl", "footer.tpl",
36}
36 37
37func makeMiddlewareSkipper(c *prometheus.PrometheusConfig) middleware.Skipper { 38func makeMiddlewareSkipper(c *prometheus.PrometheusConfig) middleware.Skipper {
38 var path string 39 var path string
@@ -54,17 +55,17 @@ func makeMiddlewareSkipper(c *prometheus.PrometheusConfig) middleware.Skipper {
54} 55}
55 56
56type EchoConfig struct { 57type EchoConfig struct {
58 Autocert glautocert.PrimingCertProvider
59 NetboxClient netbox.NetboxClient
60 ApplicationName string
61 ApplicationVersion string
62 DisableServerHeader bool
57 Debug bool 63 Debug bool
58 Hostnames []string
59 BindAddresses []string 64 BindAddresses []string
60 BindTLSAddresses []string
61 TLSCacheDir string
62 BodySizeLimit string 65 BodySizeLimit string
63 TrustedProxyIPRanges []string 66 TrustedProxyIPRanges []string
64 ManagementIPRanges []string
65 EmbeddedTemplates fs.FS 67 EmbeddedTemplates fs.FS
66 DiskTemplates fs.FS 68 DiskTemplates fs.FS
67 TemplateGlob *string
68 TemplateFunctions template.FuncMap 69 TemplateFunctions template.FuncMap
69 CombinedHostLogFile string 70 CombinedHostLogFile string
70 RedirectToWWW bool 71 RedirectToWWW bool
@@ -76,42 +77,23 @@ type EchoConfig struct {
76 77
77type EchoWrapper struct { 78type EchoWrapper struct {
78 *echo.Echo 79 *echo.Echo
79 servers []*http.Server 80 Autocert glautocert.PrimingCertProvider
80 tlsServers []*http.Server 81 middlewareJobs []glservice.RunnerFunc
81 templateFS fs.FS 82 middlewareInitJobs []glservice.SyncRunnerFunc
82 ocspErrors chan gltls.OcspError 83 servers []*http.Server
83 ocspManager *gltls.OcspManager 84 tlsServers []*http.Server
84 initDone bool 85 templateFS fs.FS
85}
86
87// Init does "expensive" work that requires network calls but must be called
88// before using the returned echo instance
89func (w *EchoWrapper) Init() error {
90 if err := w.ocspManager.Init(); err != nil {
91 return fmt.Errorf("Error loading TLS certificates and stapling: %w", err)
92 }
93 w.initDone = true
94 return nil
95} 86}
96 87
97func (w *EchoWrapper) CachedStaticRoute(prefix, path string) { 88func (w *EchoWrapper) CachedStaticRoute(prefix, path string) {
89 if w.templateFS == nil {
90 panic("Attempted to set a static route with no templates loaded")
91 }
98 StaticFS(w.GET, w.templateFS, prefix, path, glmw.CacheOneMonthMiddleware) 92 StaticFS(w.GET, w.templateFS, prefix, path, glmw.CacheOneMonthMiddleware)
99} 93}
100 94
101func (w *EchoWrapper) OcspErrors() chan gltls.OcspError {
102 return w.ocspErrors
103}
104
105func (w *EchoWrapper) RunCertificateManager(ctx context.Context, wg *sync.WaitGroup) error {
106 return w.ocspManager.Run(ctx, wg)
107}
108
109func (w *EchoWrapper) makeServerJob(s *http.Server, echoInit bool) glservice.RunnerFunc { 95func (w *EchoWrapper) makeServerJob(s *http.Server, echoInit bool) glservice.RunnerFunc {
110 return func(ctx context.Context, wg *sync.WaitGroup) error { 96 return func(ctx context.Context, wg *sync.WaitGroup) error {
111 if !w.initDone {
112 return fmt.Errorf("Echo is not initialized. Call Init()")
113 }
114
115 wg.Add(1) 97 wg.Add(1)
116 defer wg.Done() 98 defer wg.Done()
117 99
@@ -141,7 +123,7 @@ func (w *EchoWrapper) makeServerJob(s *http.Server, echoInit bool) glservice.Run
141 } 123 }
142} 124}
143 125
144func (w *EchoWrapper) MakeServerJobs() []glservice.RunnerFunc { 126func (w *EchoWrapper) makeServerJobs() []glservice.RunnerFunc {
145 out := []glservice.RunnerFunc{} 127 out := []glservice.RunnerFunc{}
146 128
147 for i, s := range w.servers { 129 for i, s := range w.servers {
@@ -158,6 +140,16 @@ func (w *EchoWrapper) MakeServerJobs() []glservice.RunnerFunc {
158 return out 140 return out
159} 141}
160 142
143func (w *EchoWrapper) AddJobsToRunner(r *service.AppRunner) {
144 r.AddInitJob(w.Autocert.PrimeCache)
145 r.AddJob(w.Autocert.PrimingReporter(w.Logger))
146
147 r.AddJobs(w.makeServerJobs())
148
149 r.AddInitJobs(w.middlewareInitJobs)
150 r.AddJobs(w.middlewareJobs)
151}
152
161func (w *EchoWrapper) GetTemplateFS() fs.FS { 153func (w *EchoWrapper) GetTemplateFS() fs.FS {
162 return w.templateFS 154 return w.templateFS
163} 155}
@@ -166,17 +158,29 @@ func (w *EchoWrapper) GetTemplateFS() fs.FS {
166// configures it in the default way that it should probably be configured in 158// configures it in the default way that it should probably be configured in
167// all cases. The struct returned from this function can be treated like a 159// all cases. The struct returned from this function can be treated like a
168// normal Echo instance (because, for the most part it is). 160// normal Echo instance (because, for the most part it is).
169//
170// Consumers must call Init() on the returned object before serving with it.
171func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) { 161func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
172 var err error 162 var err error
173 163
164 mwjobs := []glservice.RunnerFunc{}
165 mwinitjobs := []glservice.SyncRunnerFunc{}
166
167 if os.Getenv("POMONA_DC_SITE") == "" {
168 return nil, fmt.Errorf("POMONA_DC_SITE must be in the environment")
169 }
170
174 e := echo.New() 171 e := echo.New()
175 e.Debug = c.Debug 172 e.Debug = c.Debug
176 173
177 e.IPExtractor, err = glmw.ExtractIPFromXFFHeaders(false, c.TrustedProxyIPRanges) 174 // This is only required if the app is behind a proxy, for apps that
178 if err != nil { 175 // take traffic directly from the internet the IP should just be
179 return nil, fmt.Errorf("Error building XFF IP extractor: %w", err) 176 // extracted from the requests RemoteAddr.
177 if c.TrustedProxyIPRanges == nil {
178 e.IPExtractor, err = glmw.ExtractIPFromXFFHeaders(false, c.TrustedProxyIPRanges)
179 if err != nil {
180 return nil, fmt.Errorf("Error building XFF IP extractor: %w", err)
181 }
182 } else {
183 e.IPExtractor = echo.ExtractIPDirect()
180 } 184 }
181 185
182 // Use templates from disk in debug mode and the embedded ones that are 186 // Use templates from disk in debug mode and the embedded ones that are
@@ -186,21 +190,23 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
186 templates = c.DiskTemplates 190 templates = c.DiskTemplates
187 } else if c.EmbeddedTemplates != nil && !c.Debug { // Prod Mode 191 } else if c.EmbeddedTemplates != nil && !c.Debug { // Prod Mode
188 templates = c.EmbeddedTemplates 192 templates = c.EmbeddedTemplates
189 } else {
190 return nil, fmt.Errorf("No templates available for use")
191 } 193 }
192 194
193 // Only install template handlers if the path and glob are set 195 // Only install template handlers if the path and glob are set
194 if templates != nil && c.TemplateGlob != nil { 196 if templates != nil {
195 // TODO: Should assert the presence of required templates: 404.tpl
196 // 40x.tpl 50x.tpl header.tpl footer.tpl
197 e.HTTPErrorHandler = ErrorHandler(templates, c.TemplateFunctions) 197 e.HTTPErrorHandler = ErrorHandler(templates, c.TemplateFunctions)
198 198
199 tr, err := NewTemplateRenderer(templates, *c.TemplateGlob, c.TemplateFunctions) 199 tr, err := NewTemplateRenderer(templates, "*.tpl", c.TemplateFunctions)
200 if err != nil { 200 if err != nil {
201 return nil, fmt.Errorf("Error loading template renderer: %w", err) 201 return nil, fmt.Errorf("Error loading template renderer: %w", err)
202 } 202 }
203 e.Renderer = tr 203 e.Renderer = tr
204
205 for _, t := range mandatoryTemplates {
206 if !tr.HaveTemplate(e.NewContext(nil, nil), t) {
207 return nil, fmt.Errorf("Tempalate renderer is missing required template %s", t)
208 }
209 }
204 } 210 }
205 211
206 e.Logger.SetLevel(log.INFO) 212 e.Logger.SetLevel(log.INFO)
@@ -216,28 +222,27 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
216 e.Use(middleware.LoggerWithConfig(lc)) 222 e.Use(middleware.LoggerWithConfig(lc))
217 } 223 }
218 224
219 cmek := make(chan gltls.OcspError) 225 bindings, err := ParseAddressPortBindings(c.BindAddresses)
220 cm := &gltls.OcspManager{ 226 if err != nil {
221 CertPath: path.Join(c.TLSCacheDir, "cert.pem"), 227 return nil, fmt.Errorf("Error parsing address/port bindings")
222 KeyPath: path.Join(c.TLSCacheDir, "key.pem"),
223 Errors: cmek,
224 } 228 }
225 229
226 servers := make([]*http.Server, len(c.BindAddresses)) 230 servers := make([]*http.Server, len(c.BindAddresses))
227 for i, a := range c.BindAddresses { 231 for i, a := range bindings.HttpBindings() {
228 servers[i] = &http.Server{ 232 servers[i] = &http.Server{
229 Addr: a, 233 Addr: a,
230 Handler: e, 234 Handler: e,
231 } 235 }
232 } 236 }
233 237
234 tlsServers := make([]*http.Server, len(c.BindTLSAddresses)) 238 tlsServers := make([]*http.Server, len(c.BindAddresses))
235 for i, a := range c.BindTLSAddresses { 239 for i, a := range bindings.TlsBindings() {
236 tlsServers[i] = &http.Server{ 240 tlsServers[i] = &http.Server{
237 Addr: a, 241 Addr: a,
238 TLSConfig: &tls.Config{ 242 TLSConfig: &tls.Config{
239 MinVersion: tls.VersionTLS12, 243 MinVersion: tls.VersionTLS12,
240 GetCertificate: cm.GetCertificate, 244 GetCertificate: c.Autocert.GetCertificate,
245 NextProtos: []string{"h2", "http/1.1"}, // enable HTTP/2
241 }, 246 },
242 Handler: e, 247 Handler: e,
243 } 248 }
@@ -254,17 +259,9 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
254 e.Use(middleware.BodyLimit(c.BodySizeLimit)) 259 e.Use(middleware.BodyLimit(c.BodySizeLimit))
255 } 260 }
256 261
257 _, tlsPort, err := net.SplitHostPort(tlsServers[0].Addr)
258 if err != nil {
259 return nil, fmt.Errorf("Unable to split TLS addr and port: %w", err)
260 }
261 tlsPortI, err := strconv.Atoi(tlsPort)
262 if err != nil {
263 return nil, fmt.Errorf("Unable to convert TLS port to int: %w", err)
264 }
265 e.Use(glmw.HTTPSRedirectWithConfig(glmw.HTTPSRedirectConfig{ 262 e.Use(glmw.HTTPSRedirectWithConfig(glmw.HTTPSRedirectConfig{
266 Skipper: metricsSkipper, 263 Skipper: metricsSkipper,
267 Port: tlsPortI, 264 Port: bindings.TlsPort,
268 })) 265 }))
269 266
270 if c.RedirectToWWW { 267 if c.RedirectToWWW {
@@ -292,14 +289,28 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
292 if c.ContentSecurityPolicy != nil { 289 if c.ContentSecurityPolicy != nil {
293 e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy)) 290 e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy))
294 } else { 291 } else {
295 return nil, fmt.Errorf("ContentSecurityPolicy is required") 292 e.Use(glmw.ContentSecurityPolicyWithConfig(glmw.ContentSecurityPolicyConfig{
293 DefaultSrc: []glmw.CSPDirective{
294 glmw.CSPSelf,
295 },
296 }))
296 } 297 }
297 298
298 if !c.DisablePrometheus { 299 if c.ApplicationName != "" && c.ApplicationVersion != "" && !c.DisableServerHeader {
299 mips, err := glnet.ParseCIDRSlice(c.ManagementIPRanges) 300 e.Use(glmw.AddServerHeader(c.ApplicationName, c.ApplicationVersion))
300 if err != nil { 301 }
301 return nil, err 302
303 if !c.DisablePrometheus && c.NetboxClient != nil {
304 // TODO: Should constrain this by site probably but monitoring happens
305 // across sites so those prefixes need to be included
306 f := &glmw.NetboxIPFilter{
307 NetboxClient: c.NetboxClient,
308 Tag: "management",
309 IncludeLocalhost: true,
310 Logger: e.Logger,
302 } 311 }
312 mwinitjobs = append(mwinitjobs, f.Init)
313 mwjobs = append(mwjobs, f.RunRefresh)
303 314
304 var prom *prometheus.Prometheus 315 var prom *prometheus.Prometheus
305 if c.PrometheusConfig != nil { 316 if c.PrometheusConfig != nil {
@@ -309,15 +320,16 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
309 } 320 }
310 321
311 e.Use(prom.MiddlewareHandler) 322 e.Use(prom.MiddlewareHandler)
312 e.GET(prom.Config.MetricsPath, prom.MetricsHandler, glmw.NewIPFilter(mips)) 323 e.GET(prom.Config.MetricsPath, prom.MetricsHandler, f.Middleware)
313 } 324 }
314 325
315 return &EchoWrapper{ 326 return &EchoWrapper{
316 Echo: e, 327 Echo: e,
317 servers: servers, 328 Autocert: c.Autocert,
318 tlsServers: tlsServers, 329 servers: servers,
319 ocspErrors: cmek, 330 tlsServers: tlsServers,
320 ocspManager: cm, 331 templateFS: templates,
321 templateFS: templates, 332 middlewareJobs: mwjobs,
333 middlewareInitJobs: mwinitjobs,
322 }, nil 334 }, nil
323} 335}