// Package web serves the browser front end, the manual and sample programs. package web import ( "embed" "encoding/json" "io/fs" "net/http" "os" "sort" "strings" "halcyon/docs" "halcyon/examples" ) //go:embed static var static embed.FS // Handler serves the site. Mount it as the catch-all route; more specific API // routes registered on the same mux take precedence. // // manualPath is the rendered PDF manual; it is opened on every request, so a // new render is served as soon as it is renamed into place, without a restart. func Handler(manualPath string) http.Handler { sub, _ := fs.Sub(static, "static") files := http.FileServerFS(sub) mux := http.NewServeMux() mux.Handle("GET /", files) mux.HandleFunc("GET /manual.md", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/markdown; charset=utf-8") w.Write(docs.Markdown) }) mux.HandleFunc("GET /examples/index.json", func(w http.ResponseWriter, r *http.Request) { entries, _ := fs.ReadDir(examples.FS, ".") names := []string{} for _, e := range entries { if n, ok := strings.CutSuffix(e.Name(), ".s"); ok { names = append(names, n) } } sort.Strings(names) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(names) }) mux.Handle("GET /examples/", http.StripPrefix("/examples/", http.FileServerFS(examples.FS))) top := http.NewServeMux() top.HandleFunc("GET /manual.pdf", manualPDF(manualPath)) // not under the site's CSP: PDF viewers need their own resources top.Handle("/", secure(mux)) return top } // manualPDF serves the manual rendered separately from the app. func manualPDF(path string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { f, err := os.Open(path) if err != nil { http.Error(w, "The PDF manual has not been rendered yet (run scripts/render-manual.sh). "+ "The Markdown source is at /manual.md.", http.StatusNotFound) return } defer f.Close() st, err := f.Stat() if err != nil { http.Error(w, "cannot read the PDF manual", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", `inline; filename="halcyon-hc33-manual.pdf"`) w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Cache-Control", "no-cache") // revalidate, so a fresh render shows up http.ServeContent(w, r, "manual.pdf", st.ModTime(), f) // ranges and If-Modified-Since } } // secure adds headers appropriate to a page that loads only its own assets. func secure(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hd := w.Header() hd.Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'") hd.Set("X-Content-Type-Options", "nosniff") hd.Set("Referrer-Policy", "no-referrer") h.ServeHTTP(w, r) }) }