8000 feat: nextrouter pkg to handle nextjs routing rules by bryphe-coder · Pull Request #167 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: nextrouter pkg to handle nextjs routing rules #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
dd6d315
Add in-memory fs example
bryphe-coder Feb 5, 2022
33897e5
Integrate test server too
bryphe-coder Feb 5, 2022
b8d092c
Some initial test cases for files at root
bryphe-coder Feb 5, 2022
9949261
Serve contents from filesystem
bryphe-coder Feb 5, 2022
fadc0b6
Factor router out
bryphe-coder Feb 5, 2022
771f39d
Add recursive router building
bryphe-coder Feb 5, 2022
78406b1
Hook up recursive case
bryphe-coder Feb 5, 2022
50c72ba
Refactor to handle different file-serving cases
bryphe-coder Feb 5, 2022
7a98a48
Handle case w/o html extension
bryphe-coder Feb 5, 2022
1eb2402
Handle redirecting to index.html at root
bryphe-coder Feb 5, 2022
b0fd9a8
Add test case for nested path
bryphe-coder Feb 5, 2022
a506148
Remove now-unnecessary smoke test
bryphe-coder Feb 5, 2022
b59e2a8
Some clean up
bryphe-coder Feb 5, 2022
ff75695
Refactor to use http.ServeContent
bryphe-coder Feb 5, 2022
a410396
Simplify router construction
bryphe-coder Feb 5, 2022
15c38b9
rtr -> router
bryphe-coder Feb 5, 2022
c813b5e
Handle serving non-html files
bryphe-coder Feb 5, 2022
317c040
Handle trailing-slash case
bryphe-coder Feb 5, 2022
133e452
Implement dynamic routing
bryphe-coder Feb 5, 2022
6d8f412
Add test case to verify static paths are preferred over dynamic paths
bryphe-coder Feb 5, 2022
a3ecd7f
Handle dynamic routing for folders
bryphe-coder Feb 5, 2022
700c9ef
Handle catch-all routes
bryphe-coder Feb 5, 2022
05a369e
Start plumbing in a way to inject template parameters
bryphe-coder Feb 5, 2022
03a20c1
Start adding plumbing for templates
bryphe-coder Feb 5, 2022
199d468
Add template functionality
bryphe-coder Feb 5, 2022
ca515e4
Set up 404 handling
bryphe-coder Feb 5, 2022
6072bad
Use nextrouter package in site, which simplifies it quite a bit
bryphe-coder Feb 5, 2022
826eaf5
Update interface to take logger
bryphe-coder Feb 5, 2022
dac34f5
Additional comments
bryphe-coder Feb 5, 2022
92d4eb0
Fix embed_test
bryphe-coder Feb 5, 2022
5262b7f
Remove now-unused noop template func
bryphe-coder Feb 5, 2022
33342e5
Merge main
bryphe-coder Feb 8, 2022
750bec9
Switch route addition from info -> debug log level
bryphe-coder Feb 8, 2022
c15b046
Invert logic to remove layer of indentation
bryphe-coder Feb 8, 2022
c17fc15
Rename buildRoutes -> registerRoutes
bryphe-coder Feb 8, 2022
34e4f46
TemplateDataFunc -> HTMLTemplateHandler
bryphe-coder Feb 8, 2022
0743503
Add return to reduce indentation in serveFile
bryphe-coder Feb 8, 2022
4585c13
Remove call to remove file extension
bryphe-coder Feb 8, 2022
8f2b47d
Switch to register 404, return error and warn at toplevel
bryphe-coder Feb 8, 2022
19fcfac
Move ./nextrouter -> ./site/nextrouter
bryphe-coder Feb 8, 2022
8343923
Return error from handler
bryphe-coder Feb 8, 2022
4a5c353
Change dynamic route logging from Info -> Debug
bryphe-coder Feb 8, 2022
e4d031f
Merge main
bryphe-coder Feb 9, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coderd/coderd.go
8000
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func New(options *Options) http.Handler {
})
})
})
r.NotFound(site.Handler().ServeHTTP)
r.NotFound(site.Handler(options.Logger).ServeHTTP)
return r
}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
github.com/pion/logging v0.2.2
github.com/pion/transport v0.13.0
github.com/pion/webrtc/v3 v3.1.23
github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef
github.com/quasilyte/go-ruleguard/dsl v0.3.16
github.com/spf13/cobra v1.3.0
github.com/stretchr/testify v1.7.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef h1:NKxTG6GVGbfMXc2mIk+KphcH6hagbVXhcFkbTgYleTI=
github.com/psanford/memfs v0.0.0-20210214183328-a001468d78ef/go.mod h1:tcaRap0jS3eifrEEllL6ZMd9dg8IlDpi2S1oARrQ+NI=
github.com/quasilyte/go-ruleguard/dsl v0.3.16 h1:yJtIpd4oyNS+/c/gKqxNwoGO9+lPOsy1A4BzKjJRcrI=
github.com/quasilyte/go-ruleguard/dsl v0.3.16/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
Expand Down
188 changes: 21 additions & 167 deletions site/embed.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package site

import (
"bytes"
"embed"
"fmt"
"io"
"io/fs"
"net/http"
"path"
"path/filepath"
"strings"
"text/template" // html/template escapes some nonces
"time"

"github.com/justinas/nosurf"
"github.com/unrolled/secure"
"golang.org/x/xerrors"

"cdr.dev/slog"

"github.com/coder/coder/site/nextrouter"
)

// The `embed` package ignores recursively including directories
Expand All @@ -27,53 +24,33 @@ import (
var site embed.FS

// Handler returns an HTTP handler for serving the static site.
func Handler() http.Handler {
func Handler(logger slog.Logger) http.Handler {
filesystem, err := fs.Sub(site, "out")
if err != nil {
// This can't happen... Go would throw a compilation error.
panic(err)
}

// html files are handled by a text/template. Non-html files
// are served by the default file server.
files, err := htmlFiles(filesystem)
if err != nil {
panic(xerrors.Errorf("Failed to return handler for static files. Html files failed to load: %w", err))
// Render CSP and CSRF in the served pages
templateFunc := func(r *http.Request) interface{} {
return htmlState{
// Nonce is the CSP nonce for the given request (if there is one present)
CSP: cspState{Nonce: secure.CSPNonce(r.Context())},
// Token is the CSRF token for the given request
CSRF: csrfState{Token: nosurf.Token(r)},
}
}

return secureHeaders(&handler{
fs: filesystem,
htmlFiles: files,
h: http.FileServer(http.FS(filesystem)), // All other non-html static files
nextRouterHandler, err := nextrouter.Handler(filesystem, &nextrouter.Options{
Logger: logger,
TemplateDataFunc: templateFunc,
Comment on lines +44 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: I really like that the nextjs stuff is isolated into a self-contained module.

})
}

type handler struct {
fs fs.FS
// htmlFiles is the text/template for all *.html files.
// This is needed to support Content Security Policy headers.
// Due to material UI, we are forced to use a nonce to allow inline
// scripts, and that nonce is passed through a template.
// We only do this for html files to reduce the amount of in memory caching
// of duplicate files as `fs`.
htmlFiles *htmlTemplates
h http.Handler
}

// filePath returns the filepath of the requested file.
func (*handler) filePath(p string) string {
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
return strings.TrimPrefix(path.Clean(p), "/")
}

func (h *handler) exists(filePath string) bool {
f, err := h.fs.Open(filePath)
if err == nil {
_ = f.Close()
if err != nil {
// There was an error setting up our file system handler.
// This likely means a problem with our embedded file system.
panic(err)
}
return err == nil
return secureHeaders(nextRouterHandler)
}

type htmlState struct {
Expand All @@ -89,80 +66,6 @@ type csrfState struct {
Token string
}

func (h *handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// reqFile is the static file requested
reqFile := h.filePath(r.URL.Path)
state := htmlState{
// Nonce is the CSP nonce for the given request (if there is one present)
CSP: cspState{Nonce: secure.CSPNonce(r.Context())},
// Token is the CSRF token for the given request
CSRF: csrfState{Token: nosurf.Token(r)},
}

// First check if it's a file we have in our templates
if h.serveHTML(rw, r, reqFile, state) {
return
}

// If the original file path exists we serve it.
if h.exists(reqFile) {
h.h.ServeHTTP(rw, r)
return
}

// Serve the file assuming it's an html file
// This matches paths like `/app/terminal.html`
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/")
r.URL.Path += ".html"

reqFile = h.filePath(r.URL.Path)
// All html files should be served by the htmlFile templates
if h.serveHTML(rw, r, reqFile, state) {
return
}

// If we don't have the file... we should redirect to `/`
// for our single-page-app.
r.URL.Path = "/"
if h.serveHTML(rw, r, "", state) {
return
}

// This will send a correct 404
h.h.ServeHTTP(rw, r)
}

func (h *handler) serveHTML(rw http.ResponseWriter, r *http.Request, reqPath string, state htmlState) bool {
if data, err := h.htmlFiles.renderWithState(reqPath, state); err == nil {
if reqPath == "" {
// Pass "index.html" to the ServeContent so the ServeContent sets the right content headers.
reqPath = "index.html"
}
http.ServeContent(rw, r, reqPath, time.Time{}, bytes.NewReader(data))
return true
}
return false
}

type htmlTemplates struct {
tpls *template.Template
}

// renderWithState will render the file using the given nonce if the file exists
// as a template. If it does not, it will return an error.
func (t *htmlTemplates) renderWithState(filePath string, state htmlState) ([]byte, error) {
var buf bytes.Buffer
if filePath == "" {
filePath = "index.html"
}
err := t.tpls.ExecuteTemplate(&buf, filePath, state)
if err != nil {
return nil, err
}

return buf.Bytes(), nil
}

// cspDirectives is a map of all csp fetch directives to their values.
// Each directive is a set of values that is joined by a space (' ').
// All directives are semi-colon separated as a single string for the csp header.
Expand Down Expand Up @@ -264,52 +167,3 @@ func secureHeaders(next http.Handler) http.Handler {
ReferrerPolicy: "no-referrer",
}).Handler(next)
}

// htmlFiles recursively walks the file system passed finding all *.html files.
// The template returned has all html files parsed.
func htmlFiles(files fs.FS) (*htmlTemplates, error) {
// root is the collection of html templates. All templates are named by their pathing.
// So './404.html' is named '404.html'. './subdir/index.html' is 'subdir/index.html'
root := template.New("")

rootPath := "."
err := fs.WalkDir(files, rootPath, func(path string, dirEntry fs.DirEntry, err error) error {
if err != nil {
return err
}

if dirEntry.IsDir() {
return nil
}

if filepath.Ext(dirEntry.Name()) != ".html" {
return nil
}

file, err := files.Open(path)
if err != nil {
return err
}

data, err := io.ReadAll(file)
if err != nil {
return err
}

tPath := strings.TrimPrefix(path, rootPath+string(filepath.Separator))
_, err = root.New(tPath).Parse(string(data))
if err != nil {
return err
}

return nil
})

if err != nil {
return nil, err
}

return &htmlTemplates{
tpls: root,
}, nil
}
4 changes: 3 additions & 1 deletion site/embed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import (

"github.com/stretchr/testify/require"

"cdr.dev/slog"

"github.com/coder/coder/site"
)

func TestIndexPageRenders(t *testing.T) {
t.Parallel()

srv := httptest.NewServer(site.Handler())
srv := httptest.NewServer(site.Handler(slog.Logger{}))

req, err := http.NewRequestWithContext(context.Background(), "GET", srv.URL, nil)
require.NoError(t, err)
Expand Down
Loading
0