-
Notifications
You must be signed in to change notification settings - Fork 0
/
spa_handler.go
117 lines (102 loc) · 3.29 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
package main
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"regexp"
"github.com/koding/websocketproxy"
"go.uber.org/zap"
)
// SPAHandler SPAHandler config
type SPAHandler struct {
htmlRoot string
staticPathRegexp *regexp.Regexp
devMode bool
reverseProxy *httputil.ReverseProxy
websocketProxy *websocketproxy.WebsocketProxy
}
func NewSPAHandler(htmlRoot string, devMode bool) *SPAHandler {
zlog.Info("setting single page appliactio handler",
zap.String("html_root", htmlRoot),
zap.Bool("dev_mode", devMode))
host := "localhost:3000"
u, err := url.Parse(fmt.Sprintf("http://%s/", host))
if err != nil {
zlog.Fatal("parsing url", zap.Error(err), zap.String("host", host))
}
reverseProxy := httputil.NewSingleHostReverseProxy(u)
wsURL, _ := url.Parse(fmt.Sprintf("ws://%s/", host))
wsProxy := websocketproxy.NewProxy(wsURL)
return &SPAHandler{
htmlRoot: filepath.Clean(htmlRoot),
staticPathRegexp: regexp.MustCompile("^/static/"),
devMode: devMode,
reverseProxy: reverseProxy,
websocketProxy: wsProxy,
}
}
func (p *SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if p.devMode {
p.ServeHTTPForDevelopment(w, r)
} else {
p.HandleHTTPSRedirect(w, r)
p.ServeHTTPForProduction(w, r)
}
}
// ServeHTTPForDevelopment Proxies the locally running development app server
func (p *SPAHandler) ServeHTTPForDevelopment(w http.ResponseWriter, r *http.Request) {
r.Header.Del("Accept-Encoding")
if r.Header.Get("Connection") == "Upgrade" {
zlog.Debug("proxying websocket connection (upgrade)", zap.String("path", r.URL.Path))
p.websocketProxy.ServeHTTP(w, r)
} else {
zlog.Debug("proxying", zap.String("path", r.URL.Path))
p.reverseProxy.ServeHTTP(w, r)
}
}
// HandleHTTPSRedirect Redirects http to https, and adds the proper headers top HTTPS requests
func (p *SPAHandler) HandleHTTPSRedirect(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Forwarded-Proto") == "http" {
// Redirect http to https
target := "https://" + r.Host + r.URL.Path
if len(r.URL.RawQuery) > 0 {
target += "?" + r.URL.RawQuery
}
http.Redirect(w, r, target, http.StatusMovedPermanently)
} else {
w.Header().Add("Strict-Transport-Security", "max-age=600; includeSubDomains; preload")
}
}
// ServeHTTPForProduction Serves the production app
func (p *SPAHandler) ServeHTTPForProduction(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if path == "/" {
path = "/index.html"
}
var fileFound bool
filePath := filepath.Join(p.htmlRoot, path)
_, err := os.Stat(filePath)
if err == nil {
fileFound = true
}
zlog.Info("http prod serve", zap.String("file_path", path), zap.Bool("file_found", fileFound))
if fileFound && path != "/index.html" {
// If file exists, serve that file
http.FileServer(http.Dir(p.htmlRoot)).ServeHTTP(w, r)
} else if p.staticPathRegexp.MatchString(path) {
// 404 Error
http.Error(w, "resource not found", http.StatusNotFound)
} else {
// For any other request, bust cache and serve index.html
bustCache(w)
http.ServeFile(w, r, filepath.Join(p.htmlRoot, "/index.html"))
}
}
func bustCache(w http.ResponseWriter) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
}