// SPDX-FileCopyrightText: 2021 Luke Granger-Brown // // SPDX-License-Identifier: Apache-2.0 package fuphttp import ( "context" "fmt" "io/fs" "log" "net/http" "time" "github.com/google/safehtml" "github.com/google/safehtml/template" "github.com/google/safehtml/template/uncheckedconversions" shuncheckedconversions "github.com/google/safehtml/uncheckedconversions" "github.com/gorilla/mux" "gocloud.dev/blob" "hg.lukegb.com/lukegb/depot/web/fup/hashfs" ) const ( defaultRedirectExpiry = 5 * time.Minute ) type Config struct { Templates fs.FS Static fs.FS StaticRoot safehtml.TrustedResourceURL // If set, redirects to a signed URL if possible instead of serving directly. RedirectToBlobstore bool // RedirectExpiry sets the maximum lifetime of a signed URL, if RedirectToBlobstore is in use and the backend supports signed URLs. // Note that if a file has an expiry then the signed URL's validity will be capped at the expiry of the underlying file. RedirectExpiry time.Duration // Defaults to 5 minutes. // Set one of these, but not both. StorageURL string StorageBackend *blob.Bucket } type Application struct { indexTmpl *template.Template notFoundTmpl *template.Template storageBackend *blob.Bucket redirectToBlobstore bool redirectExpiry time.Duration } func (a *Application) Handler() http.Handler { r := mux.NewRouter() renderTemplate := func(t *template.Template) http.HandlerFunc { return func(rw http.ResponseWriter, r *http.Request) { if err := t.Execute(rw, nil); err != nil { log.Printf("rendering template: %v", err) } } } r.NotFoundHandler = http.HandlerFunc(a.notFound) r.HandleFunc("/", renderTemplate(a.indexTmpl)) r.HandleFunc("/raw/{filename}", a.rawDownload) return r } func (a *Application) notFound(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusNotFound) if err := a.notFoundTmpl.Execute(rw, nil); err != nil { log.Printf("rendering 404 template: %v", err) } } func (a *Application) internalError(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-type", "text/plain") rw.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(rw, "hammed server :(\n") } func parseTemplate(t *template.Template, fsys fs.FS, name string) (*template.Template, error) { bs, err := fs.ReadFile(fsys, name) if err != nil { return nil, fmt.Errorf("reading template %q: %w", name, err) } return t.ParseFromTrustedTemplate( uncheckedconversions.TrustedTemplateFromStringKnownToSatisfyTypeContract(string(bs)), ) } func loadTemplate(fsys fs.FS, name string, funcs template.FuncMap) (*template.Template, error) { t := template.New(name).Funcs(funcs) var err error if t, err = parseTemplate(t, fsys, "base.html"); err != nil { return nil, fmt.Errorf("loading base template: %w", err) } if t, err = parseTemplate(t, fsys, fmt.Sprintf("%s.html", name)); err != nil { return nil, fmt.Errorf("loading leaf template: %w", err) } return t, nil } func New(ctx context.Context, cfg *Config) (*Application, error) { a := &Application{ redirectToBlobstore: cfg.RedirectToBlobstore, redirectExpiry: cfg.RedirectExpiry, } if a.redirectExpiry == 0 { a.redirectExpiry = defaultRedirectExpiry } bkt := cfg.StorageBackend if bkt == nil { var err error if bkt, err = blob.OpenBucket(ctx, cfg.StorageURL); err != nil { return nil, fmt.Errorf("opening bucket %q: %v", cfg.StorageURL, err) } } a.storageBackend = bkt tmpls := []struct { t **template.Template name string }{ {&a.indexTmpl, "index"}, {&a.notFoundTmpl, "404"}, } funcMap := template.FuncMap{ "static": func(s string) safehtml.TrustedResourceURL { staticPath := s if fs, ok := cfg.Static.(*hashfs.FS); ok { sp, ok := fs.LookupHashedName(staticPath) if ok { staticPath = sp } else { log.Printf("warning: couldn't find static file %v", staticPath) } } return shuncheckedconversions.TrustedResourceURLFromStringKnownToSatisfyTypeContract(cfg.StaticRoot.String() + staticPath) }, } for _, tmpl := range tmpls { t, err := loadTemplate(cfg.Templates, tmpl.name, funcMap) if err != nil { return nil, fmt.Errorf("loading template %q: %w", tmpl.name, err) } *tmpl.t = t } return a, nil }