diff options
Diffstat (limited to 'echo/echo_default.go')
-rw-r--r-- | echo/echo_default.go | 180 |
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 | ||
33 | const ( | 31 | const defaultBodySizeLimit = "10M" |
34 | defaultBodySizeLimit = "10M" | 32 | |
35 | ) | 33 | var mandatoryTemplates = []string{ |
34 | "404.tpl", "40x.tpl", "50x.tpl", | ||
35 | "header.tpl", "footer.tpl", | ||
36 | } | ||
36 | 37 | ||
37 | func makeMiddlewareSkipper(c *prometheus.PrometheusConfig) middleware.Skipper { | 38 | func 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 | ||
56 | type EchoConfig struct { | 57 | type 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 | ||
77 | type EchoWrapper struct { | 78 | type 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 | ||
89 | func (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 | ||
97 | func (w *EchoWrapper) CachedStaticRoute(prefix, path string) { | 88 | func (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 | ||
101 | func (w *EchoWrapper) OcspErrors() chan gltls.OcspError { | ||
102 | return w.ocspErrors | ||
103 | } | ||
104 | |||
105 | func (w *EchoWrapper) RunCertificateManager(ctx context.Context, wg *sync.WaitGroup) error { | ||
106 | return w.ocspManager.Run(ctx, wg) | ||
107 | } | ||
108 | |||
109 | func (w *EchoWrapper) makeServerJob(s *http.Server, echoInit bool) glservice.RunnerFunc { | 95 | func (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 | ||
144 | func (w *EchoWrapper) MakeServerJobs() []glservice.RunnerFunc { | 126 | func (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 | ||
143 | func (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 | |||
161 | func (w *EchoWrapper) GetTemplateFS() fs.FS { | 153 | func (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. | ||
171 | func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) { | 161 | func 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 | } |