1
1
package site
2
2
3
3
import (
4
- "bytes"
5
4
"embed"
6
5
"fmt"
7
- "io"
8
6
"io/fs"
9
7
"net/http"
10
- "path"
11
- "path/filepath"
12
8
"strings"
13
- "text/template" // html/template escapes some nonces
14
- "time"
15
9
16
10
"github.com/justinas/nosurf"
17
11
"github.com/unrolled/secure"
18
- "golang.org/x/xerrors"
12
+
13
+ "cdr.dev/slog"
14
+
15
+ "github.com/coder/coder/site/nextrouter"
19
16
)
20
17
21
18
// The `embed` package ignores recursively including directories
@@ -27,53 +24,33 @@ import (
27
24
var site embed.FS
28
25
29
26
// Handler returns an HTTP handler for serving the static site.
30
- func Handler () http.Handler {
27
+ func Handler (logger slog. Logger ) http.Handler {
31
28
filesystem , err := fs .Sub (site , "out" )
32
29
if err != nil {
33
30
// This can't happen... Go would throw a compilation error.
34
31
panic (err )
35
32
}
36
33
37
- // html files are handled by a text/template. Non-html files
38
- // are served by the default file server.
39
- files , err := htmlFiles (filesystem )
40
- if err != nil {
41
- panic (xerrors .Errorf ("Failed to return handler for static files. Html files failed to load: %w" , err ))
34
+ // Render CSP and CSRF in the served pages
35
+ templateFunc := func (r * http.Request ) interface {} {
36
+ return htmlState {
37
+ // Nonce is the CSP nonce for the given request (if there is one present)
38
+ CSP : cspState {Nonce : secure .CSPNonce (r .Context ())},
39
+ // Token is the CSRF token for the given request
40
+ CSRF : csrfState {Token : nosurf .Token (r )},
41
+ }
42
42
}
43
43
44
- return secureHeaders (& handler {
45
- fs : filesystem ,
46
- htmlFiles : files ,
47
- h : http .FileServer (http .FS (filesystem )), // All other non-html static files
44
+ nextRouterHandler , err := nextrouter .Handler (filesystem , & nextrouter.Options {
45
+ Logger : logger ,
46
+ TemplateDataFunc : templateFunc ,
48
47
})
49
- }
50
-
51
- type handler struct {
52
- fs fs.FS
53
- // htmlFiles is the text/template for all *.html files.
54
- // This is needed to support Content Security Policy headers.
55
- // Due to material UI, we are forced to use a nonce to allow inline
56
- // scripts, and that nonce is passed through a template.
57
- // We only do this for html files to reduce the amount of in memory caching
58
- // of duplicate files as `fs`.
59
- htmlFiles * htmlTemplates
60
- h http.Handler
61
- }
62
-
63
- // filePath returns the filepath of the requested file.
64
- func (* handler ) filePath (p string ) string {
65
- if ! strings .HasPrefix (p , "/" ) {
66
- p = "/" + p
67
- }
68
- return strings .TrimPrefix (path .Clean (p ), "/" )
69
- }
70
-
71
- func (h * handler ) exists (filePath string ) bool {
72
- f , err := h .fs .Open (filePath )
73
- if err == nil {
74
- _ = f .Close ()
48
+ if err != nil {
49
+ // There was an error setting up our file system handler.
50
+ // This likely means a problem with our embedded file system.
51
+ panic (err )
75
52
}
76
- return err == nil
53
+ return secureHeaders ( nextRouterHandler )
77
54
}
78
55
79
56
type htmlState struct {
@@ -89,80 +66,6 @@ type csrfState struct {
89
66
Token string
90
67
}
91
68
92
- func (h * handler ) ServeHTTP (rw http.ResponseWriter , r * http.Request ) {
93
- // reqFile is the static file requested
94
- reqFile := h .filePath (r .URL .Path )
95
- state := htmlState {
96
- // Nonce is the CSP nonce for the given request (if there is one present)
97
- CSP : cspState {Nonce : secure .CSPNonce (r .Context ())},
98
- // Token is the CSRF token for the given request
99
- CSRF : csrfState {Token : nosurf .Token (r )},
100
- }
101
-
102
- // First check if it's a file we have in our templates
103
- if h .serveHTML (rw , r , reqFile , state ) {
104
- return
105
- }
106
-
107
- // If the original file path exists we serve it.
108
- if h .exists (reqFile ) {
109
- h .h .ServeHTTP (rw , r )
110
- return
111
- }
112
-
113
- // Serve the file assuming it's an html file
114
- // This matches paths like `/app/terminal.html`
115
- r .URL .Path = strings .TrimSuffix (r .URL .Path , "/" )
116
- r .URL .Path += ".html"
117
-
118
- reqFile = h .filePath (r .URL .Path )
119
- // All html files should be served by the htmlFile templates
120
- if h .serveHTML (rw , r , reqFile , state ) {
121
- return
122
- }
123
-
124
- // If we don't have the file... we should redirect to `/`
125
- // for our single-page-app.
126
- r .URL .Path = "/"
127
- if h .serveHTML (rw , r , "" , state ) {
128
- return
129
- }
130
-
131
- // This will send a correct 404
132
- h .h .ServeHTTP (rw , r )
133
- }
134
-
135
- func (h * handler ) serveHTML (rw http.ResponseWriter , r * http.Request , reqPath string , state htmlState ) bool {
136
- if data , err := h .htmlFiles .renderWithState (reqPath , state ); err == nil {
137
- if reqPath == "" {
138
- // Pass "index.html" to the ServeContent so the ServeContent sets the right content headers.
139
- reqPath = "index.html"
140
- }
141
- http .ServeContent (rw , r , reqPath , time.Time {}, bytes .NewReader (data ))
142
- return true
143
- }
144
- return false
145
- }
146
-
147
- type htmlTemplates struct {
148
- tpls * template.Template
149
- }
150
-
151
- // renderWithState will render the file using the given nonce if the file exists
152
- // as a template. If it does not, it will return an error.
153
- func (t * htmlTemplates ) renderWithState (filePath string , state htmlState ) ([]byte , error ) {
154
- var buf bytes.Buffer
155
- if filePath == "" {
156
- filePath = "index.html"
157
- }
158
- err := t .tpls .ExecuteTemplate (& buf , filePath , state )
159
- if err != nil {
160
- return nil , err
161
- }
162
-
163
- return buf .Bytes (), nil
164
- }
165
-
166
69
// cspDirectives is a map of all csp fetch directives to their values.
167
70
// Each directive is a set of values that is joined by a space (' ').
168
71
// All directives are semi-colon separated as a single string for the csp header.
@@ -264,52 +167,3 @@ func secureHeaders(next http.Handler) http.Handler {
264
167
ReferrerPolicy : "no-referrer" ,
265
168
}).Handler (next )
266
169
}
267
-
268
- // htmlFiles recursively walks the file system passed finding all *.html files.
269
- // The template returned has all html files parsed.
270
- func htmlFiles (files fs.FS ) (* htmlTemplates , error ) {
271
- // root is the collection of html templates. All templates are named by their pathing.
272
- // So './404.html' is named '404.html'. './subdir/index.html' is 'subdir/index.html'
273
- root := template .New ("" )
274
-
275
- rootPath := "."
276
- err := fs .WalkDir (files , rootPath , func (path string , dirEntry fs.DirEntry , err error ) error {
277
- if err != nil {
278
- return err
279
- }
280
-
281
- if dirEntry .IsDir () {
282
- return nil
283
- }
284
-
285
- if filepath .Ext (dirEntry .Name ()) != ".html" {
286
- return nil
287
- }
288
-
289
- file , err := files .Open (path )
290
- if err != nil {
291
- return err
292
- }
293
-
294
- data , err := io .ReadAll (file )
295
- if err != nil {
296
- return err
297
- }
298
-
299
- tPath := strings .TrimPrefix (path , rootPath + string (filepath .Separator ))
300
- _ , err = root .New (tPath ).Parse (string (data ))
301
- if err != nil {
302
- return err
303
- }
304
-
305
- return nil
306
- })
307
-
308
- if err != nil {
309
- return nil , err
310
- }
311
-
312
- return & htmlTemplates {
313
- tpls : root ,
314
- }, nil
315
- }
0 commit comments