rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the bump and the option have to land together: the option does not exist in v1.22.0 and the panic fires at construction, inside routes(), which makes it a startup failure rather than a request-time one. The wildcard stays. The comment widget is embedded on arbitrary third-party sites, so the set of origins is not knowable, which is why the escape hatch was asked for upstream instead of accepting the panic. What it costs is unchanged and now written next to the call: any site a signed-in user visits can read authenticated responses, so state-changing requests have to keep being protected by something other than the origin, X-XSRF-Token today. The example module is tidied in the same commit, as it reaches go-pkgz/rest through the replace directive and its indirect graph would otherwise keep the old pin and fail the readonly module check in CI. The bump also carries testify to v1.12.0, which drops go-spew and go-difflib from the module graph.
192 lines
5.1 KiB
Go
192 lines
5.1 KiB
Go
package rest
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// FS provides http.FileServer handler to serve static files from a http.FileSystem,
|
|
// prevents directory listing by default and supports spa-friendly mode (off by default) returning /index.html on 404.
|
|
// - public defines base path of the url, i.e. for http://example.com/static/* it should be /static
|
|
// - local for the local path to the root of the served directory
|
|
// - notFound is the reader for the custom 404 html, can be nil for default
|
|
type FS struct {
|
|
public, root string
|
|
notFound io.Reader
|
|
isSpa bool
|
|
enableListing bool
|
|
handler http.HandlerFunc
|
|
}
|
|
|
|
// NewFileServer creates file server with optional spa mode and optional direcroty listing (disabled by default)
|
|
func NewFileServer(public, local string, options ...FsOpt) (*FS, error) {
|
|
res := FS{
|
|
public: public,
|
|
notFound: nil,
|
|
isSpa: false,
|
|
enableListing: false,
|
|
}
|
|
|
|
root, err := filepath.Abs(local)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't get absolute path for %s: %w", local, err)
|
|
}
|
|
res.root = root
|
|
|
|
if _, err = os.Stat(root); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("local path %s doesn't exist: %w", root, err)
|
|
}
|
|
|
|
for _, opt := range options {
|
|
err = opt(&res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
cfs := customFS{
|
|
fs: http.Dir(root),
|
|
spa: res.isSpa,
|
|
listing: res.enableListing,
|
|
}
|
|
f := http.StripPrefix(public, http.FileServer(cfs))
|
|
res.handler = func(w http.ResponseWriter, r *http.Request) { f.ServeHTTP(w, r) }
|
|
|
|
if !res.enableListing {
|
|
h, err := custom404Handler(f, res.notFound)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.handler = func(w http.ResponseWriter, r *http.Request) { h.ServeHTTP(w, r) }
|
|
}
|
|
|
|
return &res, nil
|
|
}
|
|
|
|
// FileServer is a shortcut for making FS with listing disabled and the custom noFound reader (can be nil).
|
|
//
|
|
// Deprecated: the method is for back-compatibility only and user should use the universal NewFileServer instead
|
|
func FileServer(public, local string, notFound io.Reader) (http.Handler, error) {
|
|
return NewFileServer(public, local, FsOptCustom404(notFound))
|
|
}
|
|
|
|
// FileServerSPA is a shortcut for making FS with SPA-friendly handling of 404, listing disabled and the custom noFound reader (can be nil).
|
|
//
|
|
// Deprecated: the method is for back-compatibility only and user should use the universal NewFileServer instead
|
|
func FileServerSPA(public, local string, notFound io.Reader) (http.Handler, error) {
|
|
return NewFileServer(public, local, FsOptCustom404(notFound), FsOptSPA)
|
|
}
|
|
|
|
// ServeHTTP makes FileServer compatible with http.Handler interface
|
|
func (fs *FS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
fs.handler(w, r)
|
|
}
|
|
|
|
// FsOpt defines functional option type
|
|
type FsOpt func(fs *FS) error
|
|
|
|
// FsOptSPA turns on SPA mode returning "/index.html" on not-found
|
|
func FsOptSPA(fs *FS) error {
|
|
fs.isSpa = true
|
|
return nil
|
|
}
|
|
|
|
// FsOptListing turns on directory listing
|
|
func FsOptListing(fs *FS) error {
|
|
fs.enableListing = true
|
|
return nil
|
|
}
|
|
|
|
// FsOptCustom404 sets custom 404 reader
|
|
func FsOptCustom404(fr io.Reader) FsOpt {
|
|
return func(fs *FS) error {
|
|
fs.notFound = fr
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// customFS wraps http.FileSystem with spa and no-listing optional support
|
|
type customFS struct {
|
|
fs http.FileSystem
|
|
spa bool
|
|
listing bool
|
|
}
|
|
|
|
// Open file on FS, for directory enforce index.html and fail on a missing index.
|
|
// Every handle opened here is either returned to the caller or closed, as http.FileServer
|
|
// closes only the file it gets back.
|
|
func (cfs customFS) Open(name string) (http.File, error) {
|
|
|
|
f, err := cfs.fs.Open(name)
|
|
if err != nil {
|
|
if cfs.spa {
|
|
return cfs.fs.Open("/index.html")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
finfo, err := f.Stat()
|
|
if err != nil {
|
|
_ = f.Close()
|
|
return nil, err
|
|
}
|
|
|
|
if finfo.IsDir() {
|
|
index := strings.TrimSuffix(name, "/") + "/index.html"
|
|
indexFile, ierr := cfs.fs.Open(index)
|
|
if ierr == nil { // index.html will be served if found
|
|
_ = indexFile.Close() // opened to probe for existence only, http.FileServer opens it again on its own
|
|
return f, nil
|
|
}
|
|
// no index.html in directory
|
|
if !cfs.listing { // listing disabled
|
|
_ = f.Close()
|
|
return nil, ierr
|
|
}
|
|
}
|
|
|
|
return f, nil
|
|
}
|
|
|
|
// respWriter404 intercept Write to provide custom 404 response
|
|
type respWriter404 struct {
|
|
http.ResponseWriter
|
|
status int
|
|
msg []byte
|
|
}
|
|
|
|
func (w *respWriter404) WriteHeader(status int) {
|
|
w.status = status
|
|
if status == http.StatusNotFound {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
}
|
|
w.ResponseWriter.WriteHeader(status)
|
|
}
|
|
|
|
func (w *respWriter404) Write(p []byte) (n int, err error) {
|
|
if w.status != http.StatusNotFound || w.msg == nil {
|
|
return w.ResponseWriter.Write(p)
|
|
}
|
|
_, err = w.ResponseWriter.Write(w.msg)
|
|
return len(p), err
|
|
}
|
|
|
|
func custom404Handler(next http.Handler, notFound io.Reader) (http.Handler, error) {
|
|
if notFound == nil {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) }), nil
|
|
}
|
|
|
|
body, err := io.ReadAll(notFound)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
next.ServeHTTP(&respWriter404{ResponseWriter: w, msg: body}, r)
|
|
}), nil
|
|
}
|