depot/web/fup/fuphttp/fuphttp.go
Luke Granger-Brown 7eac2c0c07 fup/fuphttp: add badRequest handler
We might get bad requests for e.g. file uploads, so we should have an error
handler for that.

This also disables mime type sniffing for the other text/plain error handler.
2021-03-21 17:08:29 +00:00

176 lines
4.9 KiB
Go

// SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
//
// 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/fuphttp/fngen"
"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
// FilenameGenerator returns a new filename based on the provided prefix and extension.
FilenameGenerator fngen.FilenameGenerator
}
type Application struct {
indexTmpl *template.Template
notFoundTmpl *template.Template
storageBackend *blob.Bucket
redirectToBlobstore bool
redirectExpiry time.Duration
filenameGenerator fngen.FilenameGenerator
}
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; charset=utf-8")
rw.Header().Set("X-Content-Type-Options", "nosniff")
rw.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(rw, "hammed server :(\n")
}
func (a *Application) badRequest(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Set("Content-type", "text/plain; charset=utf-8")
rw.Header().Set("X-Content-Type-Options", "nosniff")
rw.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(rw, "bad request: %v\n", err.Error())
}
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,
filenameGenerator: cfg.FilenameGenerator,
}
if a.redirectExpiry == 0 {
a.redirectExpiry = defaultRedirectExpiry
}
if a.filenameGenerator == nil {
a.filenameGenerator = fngen.PetnameGenerator
}
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
}