/
spa_handler.go
119 lines (102 loc) · 3.31 KB
/
spa_handler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package handlers
import (
"net/http"
"path/filepath"
"github.com/spf13/afero"
"go.uber.org/zap"
"github.com/transcom/mymove/pkg/logging"
)
// The SpaHandler code was inspired by code from the README for
// github.com/gorilla/mux
//
// https://github.com/gorilla/mux#serving-single-page-applications
// SpaHandler implements the http.Handler interface, so we can use it
// to respond to HTTP requests. The path to the static directory and
// path to the index file within that static directory are used to
// serve the SPA in the given static directory.
type SpaHandler struct {
staticPath string
indexPath string
cfs CustomFileSystem
}
// NewSpaHandler returns a new handler for a Single Page App
func NewSpaHandler(staticPath string, indexPath string, cfs CustomFileSystem) SpaHandler {
return SpaHandler{
staticPath: staticPath,
indexPath: indexPath,
cfs: cfs,
}
}
// from https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings
type CustomFileSystem struct {
fs http.FileSystem
indexPath string
logger *zap.Logger
}
func NewCustomFileSystem(fs http.FileSystem, indexPath string, logger *zap.Logger) CustomFileSystem {
return CustomFileSystem{
fs: fs,
indexPath: indexPath,
logger: logger,
}
}
func (cfs CustomFileSystem) Open(path string) (http.File, error) {
f, openErr := cfs.fs.Open(path)
logger := cfs.logger
logger.Debug("Using CustomFileSystem for " + path)
if openErr != nil {
logger.Error("Error with opening", zap.Error(openErr))
return nil, openErr
}
s, _ := f.Stat()
if s.IsDir() {
index := filepath.Join(path, cfs.indexPath)
if _, indexOpenErr := cfs.fs.Open(index); indexOpenErr != nil {
closeErr := f.Close()
if closeErr != nil {
logger.Error("Unable to close ", zap.Error(closeErr))
return nil, closeErr
}
logger.Error("Unable to open index.html in the directory", zap.Error(indexOpenErr))
return nil, indexOpenErr
}
}
return f, nil
}
// ServeHTTP inspects the URL path to locate a file within the static dir
// on the SPA handler. If a file is found, it will be served. If not, the
// file located at the index path on the SPA handler will be served. This
// is suitable behavior for serving an SPA (single page application).
func (h SpaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logger := logging.FromContext(r.Context())
logger.Debug("Using SPA Handler for " + r.URL.Path)
// get the absolute path to prevent directory traversal
_, err := filepath.Abs(r.URL.Path)
if err != nil {
// if we failed to get the absolute path respond with a 400 bad request
// and stop
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// otherwise, use http.FileServer to serve the static dir
// use the customFileSystem so that we do not expose directory listings
http.FileServer(h.cfs).ServeHTTP(w, r)
}
// NewFileHandler serves up a single file from a custom filesystem.
// Use the custom filesystem for ease of testing
func NewFileHandler(fs afero.Fs, entrypoint string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
info, err := fs.Stat(entrypoint)
if err != nil {
http.NotFound(w, r)
return
}
f, err := fs.Open(entrypoint)
if err != nil {
http.NotFound(w, r)
return
}
defer f.Close()
http.ServeContent(w, r, entrypoint, info.ModTime(), f)
}
}