From ef4444e6217592e2666b8a46e8c762eaf93fa8a6 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Tue, 9 Nov 2021 17:43:17 -0800 Subject: Convert templates to use FS interface --- echo/echo_default.go | 37 ++++++++++++++++++------ echo/error_handler.go | 17 ++++++------ echo/static_file.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++ echo/template_renderer.go | 25 +++++++---------- tls/ocsp.go | 1 + 5 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 echo/static_file.go diff --git a/echo/echo_default.go b/echo/echo_default.go index ab163ff..895ee6e 100644 --- a/echo/echo_default.go +++ b/echo/echo_default.go @@ -5,7 +5,9 @@ import ( "crypto/tls" "fmt" "html/template" + "io/fs" "net/http" + "path" "sync" glmw "code.crute.us/mcrute/golib/echo/middleware" @@ -20,10 +22,10 @@ type EchoConfig struct { Debug bool BindAddress string BindTLSAddress string - TLSCert string - TLSKey string + TLSCacheDir string TrustedProxyIPRanges []string - TemplatePath *string + EmbeddedTemplates fs.FS + DiskTemplates fs.FS TemplateGlob *string TemplateFunctions template.FuncMap CombinedHostLogFile string @@ -34,6 +36,7 @@ type EchoConfig struct { type EchoWrapper struct { *echo.Echo tlsServer http.Server + templateFS fs.FS bindAddress string ocspErrors chan gltls.OcspError ocspManager *gltls.OcspManager @@ -51,7 +54,7 @@ func (w *EchoWrapper) Init() error { } func (w *EchoWrapper) CachedStaticRoute(prefix, path string) { - w.Group(prefix, glmw.CacheOneMonthMiddleware).Static("/", path) + StaticFS(w.GET, w.templateFS, prefix, path, glmw.CacheOneMonthMiddleware) } func (w *EchoWrapper) OcspErrors() chan gltls.OcspError { @@ -103,6 +106,10 @@ func (w *EchoWrapper) ServeTLS(ctx context.Context, wg *sync.WaitGroup) error { ) } +func (w *EchoWrapper) GetTemplateFS() fs.FS { + return w.templateFS +} + // NewDefaultEchoWithConfig builds a wrapper around an Echo instance and // configures it in the default way that it should probably be configured in // all cases. The struct returned from this function can be treated like a @@ -120,11 +127,22 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) { return nil, fmt.Errorf("Error building XFF IP extractor: %w", err) } + // Use templates from disk in debug mode and the embedded ones that are + // built-in to the binary for prod mode. + var templates fs.FS + if c.DiskTemplates != nil && c.Debug { // Debug Mode + templates = c.DiskTemplates + } else if c.EmbeddedTemplates != nil && !c.Debug { // Prod Mode + templates = c.EmbeddedTemplates + } else { + return nil, fmt.Errorf("No templates available for use") + } + // Only install template handlers if the path and glob are set - if c.TemplatePath != nil && c.TemplateGlob != nil { - e.HTTPErrorHandler = ErrorHandler(*c.TemplatePath, c.TemplateFunctions) + if templates != nil && c.TemplateGlob != nil { + e.HTTPErrorHandler = ErrorHandler(templates, c.TemplateFunctions) - tr, err := NewTemplateRenderer(*c.TemplatePath, *c.TemplateGlob, c.TemplateFunctions) + tr, err := NewTemplateRenderer(templates, *c.TemplateGlob, c.TemplateFunctions) if err != nil { return nil, fmt.Errorf("Error loading template renderer: %w", err) } @@ -146,8 +164,8 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) { cmek := make(chan gltls.OcspError) cm := &gltls.OcspManager{ - CertPath: c.TLSCert, - KeyPath: c.TLSKey, + CertPath: path.Join(c.TLSCacheDir, "cert.pem"), + KeyPath: path.Join(c.TLSCacheDir, "key.pem"), Errors: cmek, } @@ -181,5 +199,6 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) { bindAddress: c.BindAddress, ocspErrors: cmek, ocspManager: cm, + templateFS: templates, }, nil } diff --git a/echo/error_handler.go b/echo/error_handler.go index c32fe86..bfbcfb6 100644 --- a/echo/error_handler.go +++ b/echo/error_handler.go @@ -4,14 +4,14 @@ import ( "bytes" "fmt" "html/template" + "io/fs" "net/http" - "path" "github.com/labstack/echo/v4" ) // Copied from echo and tweaked to make our errors nicer -func ErrorHandler(templatePath string, funcs template.FuncMap) func(error, echo.Context) { +func ErrorHandler(templates fs.FS, funcs template.FuncMap) func(error, echo.Context) { return func(err error, c echo.Context) { defer func() { if r := recover(); r != nil { @@ -37,12 +37,13 @@ func ErrorHandler(templatePath string, funcs template.FuncMap) func(error, echo. } } - t, err := template.New("").Funcs(funcs).ParseFiles( - path.Join(templatePath, "404.tpl"), - path.Join(templatePath, "40x.tpl"), - path.Join(templatePath, "50x.tpl"), - path.Join(templatePath, "header.tpl"), - path.Join(templatePath, "footer.tpl"), + t, err := template.New("").Funcs(funcs).ParseFS( + templates, + "404.tpl", + "40x.tpl", + "50x.tpl", + "header.tpl", + "footer.tpl", ) if err != nil { he = &echo.HTTPError{ diff --git a/echo/static_file.go b/echo/static_file.go new file mode 100644 index 0000000..9723db7 --- /dev/null +++ b/echo/static_file.go @@ -0,0 +1,71 @@ +package echo + +import ( + "io" + "io/fs" + "net/http" + "net/url" + "path/filepath" + + "github.com/labstack/echo/v4" +) + +type routeFunc func(string, echo.HandlerFunc, ...echo.MiddlewareFunc) *echo.Route + +func StaticFS(get routeFunc, f fs.FS, prefix, root string, m ...echo.MiddlewareFunc) *echo.Route { + if root == "" { + root = "." // For security we want to restrict to CWD. + } + + h := func(c echo.Context) error { + p, err := url.PathUnescape(c.Param("*")) + if err != nil { + return err + } + + name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security + fp, err := f.Open(name) + if err != nil { + // The access path does not exist + return echo.NotFoundHandler(c) + } + defer fp.Close() + + fi, err := fp.Stat() + if err != nil { + // The access path does not exist + return echo.NotFoundHandler(c) + } + + // If the request is for a directory and does not end with "/" + p = c.Request().URL.Path // path must not be empty. + if fi.IsDir() && p[len(p)-1] != '/' { + // Redirect to ends with "/" + // return c.Redirect(http.StatusMovedPermanently, p+"/") + // TODO: Serve an index.html if there is one for this dir + return echo.NotFoundHandler(c) + } + + fs, ok := fp.(io.ReadSeeker) + if !ok { + c.Logger().Errorf("File %s is not a io.ReadSeeker", p) + return echo.ErrInternalServerError + } + + http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), fs) + return nil + } + + // Handle added routes based on trailing slash: + // /prefix => exact route "/prefix" + any route "/prefix/*" + // /prefix/ => only any route "/prefix/*" + if prefix != "" { + if prefix[len(prefix)-1] == '/' { + // Only add any route for intentional trailing slash + return get(prefix+"*", h, m...) + } + get(prefix, h, m...) + } + + return get(prefix+"/*", h, m...) +} diff --git a/echo/template_renderer.go b/echo/template_renderer.go index 0bafd92..7bfd006 100644 --- a/echo/template_renderer.go +++ b/echo/template_renderer.go @@ -3,8 +3,8 @@ package echo import ( "html/template" "io" + "io/fs" "net/http" - "os" "path/filepath" "github.com/labstack/echo/v4" @@ -15,16 +15,16 @@ type TemplateChecker interface { } type TemplateRenderer struct { - path string - glob string - funcs template.FuncMap - cache *template.Template + templates fs.FS + glob string + funcs template.FuncMap + cache *template.Template } -func NewTemplateRenderer(path, glob string, funcs template.FuncMap) (*TemplateRenderer, error) { +func NewTemplateRenderer(templates fs.FS, glob string, funcs template.FuncMap) (*TemplateRenderer, error) { var err error - r := &TemplateRenderer{path: path, glob: glob, funcs: funcs} + r := &TemplateRenderer{templates: templates, glob: glob, funcs: funcs} r.cache, err = r.loadTemplates() if err != nil { return nil, err @@ -36,7 +36,7 @@ func NewTemplateRenderer(path, glob string, funcs template.FuncMap) (*TemplateRe func (r *TemplateRenderer) loadTemplates() (*template.Template, error) { t := template.New("").Funcs(r.funcs) - err := filepath.Walk(r.path, func(path string, info os.FileInfo, err error) error { + err := fs.WalkDir(r.templates, ".", func(path string, info fs.DirEntry, err error) error { if err != nil { return err } @@ -44,17 +44,12 @@ func (r *TemplateRenderer) loadTemplates() (*template.Template, error) { if !info.IsDir() { _, fn := filepath.Split(path) if ok, _ := filepath.Match(r.glob, fn); ok { - fc, err := os.ReadFile(path) + fc, err := fs.ReadFile(r.templates, path) if err != nil { return err } - rp, err := filepath.Rel(r.path, path) - if err != nil { - return err - } - - _, err = t.New(rp).Parse(string(fc)) + _, err = t.New(path).Parse(string(fc)) if err != nil { return err } diff --git a/tls/ocsp.go b/tls/ocsp.go index 94f7f68..b80764f 100644 --- a/tls/ocsp.go +++ b/tls/ocsp.go @@ -125,6 +125,7 @@ func (m *OcspManager) Run(ctx context.Context, wg *sync.WaitGroup) error { } } +// TODO: TLS.GetCertificate for dyanmic certs for LE (cache these) func (m *OcspManager) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) { m.RLock() defer m.RUnlock() -- cgit v1.2.3