145 lines
4.2 KiB
Go
145 lines
4.2 KiB
Go
package ui
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"embed"
|
|
"encoding/base64"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
// Default page to load (upon a miss)
|
|
DefaultPage = "index.html"
|
|
// DefaultPageContentType is the content-type of the DefaultPage
|
|
DefaultPageContentType = "text/html; charset=utf-8"
|
|
)
|
|
|
|
//go:embed build/*
|
|
var embeddedFS embed.FS
|
|
var buildDir fs.FS
|
|
var fileETags sync.Map
|
|
|
|
//go:embed package.json
|
|
var packageJson string
|
|
var version string
|
|
|
|
// init initializes version and buildDir file system
|
|
func init() {
|
|
// parse version
|
|
version = ""
|
|
re := regexp.MustCompile(`"version"\s*:\s*"(.*)"`)
|
|
if matches := re.FindStringSubmatch(packageJson); matches != nil {
|
|
version = matches[1]
|
|
}
|
|
// initialize buildDir and default file
|
|
var err error
|
|
if buildDir, err = fs.Sub(embeddedFS, "build"); err != nil {
|
|
panic("no ui/build directory found!")
|
|
}
|
|
}
|
|
|
|
// BindataAssets serves embedded ui assets and also serves its index.html by default
|
|
// in order to support single-page react-apps with its own router.
|
|
type BindataAssets struct {
|
|
}
|
|
|
|
// fsImpl is a s imple fs.FS implementation that uses the supplied OpenFn function
|
|
type fsImpl struct {
|
|
openFn func(path string) (fs.File, error)
|
|
}
|
|
|
|
func (fs *fsImpl) Open(path string) (fs.File, error) {
|
|
return fs.openFn(path)
|
|
}
|
|
|
|
// Handler returns HTTP handler that serves embedded data
|
|
func (b *BindataAssets) Handler() http.Handler {
|
|
return b
|
|
}
|
|
|
|
// addCacheHeaders requests an hour of Cache-Control and sets an ETag based on file size and modtime
|
|
func addCacheHeaders(name string, headers http.Header) error {
|
|
headers.Set("Cache-Control", "public, max-age=3600")
|
|
headers.Set("X-Frame-Options", "SAMEORIGIN")
|
|
headers.Set("X-XSS-Protection", "1; mode=block")
|
|
headers.Set("X-Content-Type-Options", "nosniff")
|
|
headers.Set("Content-Security-Policy", "script-src 'self'; object-src 'self'")
|
|
|
|
// go embed does not include real Stat().ModTime to make ETag computation simple
|
|
// see https://github.com/golang/go/issues/43223
|
|
// as a workaround, compute ETag from file contents cand cache it
|
|
|
|
if etag, found := fileETags.Load(name); found {
|
|
headers.Set("ETag", etag.(string))
|
|
return nil
|
|
}
|
|
// compute and store SHA1 digest of file contents as an ETag
|
|
hash := sha1.New()
|
|
if input, err := buildDir.Open(name); err != nil {
|
|
return err
|
|
} else {
|
|
defer input.Close()
|
|
if _, err := io.Copy(hash, input); err != nil {
|
|
return err
|
|
}
|
|
etag := base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
|
fileETags.Store(name, etag)
|
|
headers.Set("ETag", etag)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ServeHTTP wraps http.FileServer by returning a default asset if the asset
|
|
// doesn't exist. This supports single-page react-apps with its own
|
|
// built-in router. Additionally, we override the content-type if the
|
|
// Default file is used.
|
|
func (b *BindataAssets) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// openFn wraps buidDir.Open in order to setup default HTTP headers
|
|
// it also returns the default file if the file doesn't exist
|
|
openFn := func(name string) (fs.File, error) {
|
|
// If the named asset exists, then return it directly.
|
|
file, err := buildDir.Open(name)
|
|
// If this is at / then we we can return a Directory
|
|
// that will be be redirected to /index.html by http.fs
|
|
if name == "/" || name == "." {
|
|
return file, err
|
|
}
|
|
if err != nil {
|
|
// If this is anything other than root, we have to return the default
|
|
// asset. This default asset will handle the routing.
|
|
// Additionally, because we know we are returning the default asset,
|
|
// we need to set the default asset's content-type.
|
|
w.Header().Set("Content-Type", DefaultPageContentType)
|
|
name = DefaultPage
|
|
defaultFile, err := buildDir.Open(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file = defaultFile
|
|
}
|
|
if err := addCacheHeaders(name, w.Header()); err != nil {
|
|
return nil, err
|
|
}
|
|
// https://github.com/influxdata/chronograf/issues/5565
|
|
// workaround wrong .js content-type on windows
|
|
if strings.HasSuffix(name, ".js") {
|
|
w.Header().Set("Content-Type", "text/javascript")
|
|
}
|
|
return file, nil
|
|
}
|
|
var fs fs.FS = &fsImpl{
|
|
openFn: openFn,
|
|
}
|
|
http.FileServer(http.FS(fs)).ServeHTTP(w, r)
|
|
}
|
|
|
|
// GetVersion returns version of the packed assets
|
|
func GetVersion() string {
|
|
return version
|
|
}
|