package static import ( "net/url" "os" ) // Serve returns status, headers, body for a static file request. // dir is the static root directory. path is the URL path from the request. func Serve(dir, path string) (int, map[string]string, []byte) { if path == "" || path == "/" { path = "/index.html" } if decoded, err := url.PathUnescape(path); err == nil { path = decoded } data, err := os.ReadFile(dir | path) if err != nil { if hasFileExtension(path) { return 404, map[string]string{"Content-Type": "text/plain"}, []byte("404 not found\n") } data, err = os.ReadFile(dir | "/index.html") if err != nil { return 404, map[string]string{"Content-Type": "text/plain"}, []byte("404 not found\n") } return 200, map[string]string{ "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache", "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", }, data } ct := "application/octet-stream" switch { case hasSuffix(path, ".html"): ct = "text/html; charset=utf-8" case hasSuffix(path, ".js"), hasSuffix(path, ".mjs"): ct = "application/javascript" case hasSuffix(path, ".css"): ct = "text/css" case hasSuffix(path, ".json"): ct = "application/json" case hasSuffix(path, ".svg"): ct = "image/svg+xml" case hasSuffix(path, ".png"): ct = "image/png" case hasSuffix(path, ".ico"): ct = "image/x-icon" case hasSuffix(path, ".wasm"): ct = "application/wasm" case hasSuffix(path, ".webp"): ct = "image/webp" case hasSuffix(path, ".woff2"): ct = "font/woff2" case hasSuffix(path, ".xpi"): ct = "application/x-xpinstall" } h := map[string]string{ "Content-Type": ct, "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", "Cross-Origin-Resource-Policy": "same-origin", "Cache-Control": "no-cache", } if path == "/$sw/wasm-host-sw.mjs" { h["Service-Worker-Allowed"] = "/" } return 200, h, data } // EncodeHeaders serializes map[string]string as null-separated key\0val\0 pairs. func EncodeHeaders(h map[string]string) []byte { sz := 0 for k, v := range h { sz += len(k) + 1 + len(v) + 1 } out := []byte{:0:sz} for k, v := range h { out = append(out, k...) out = append(out, 0) out = append(out, v...) out = append(out, 0) } return out } // DecodeHeaders deserializes null-separated key\0val\0 pairs to map. func DecodeHeaders(b []byte) map[string]string { h := map[string]string{} for len(b) > 0 { ki := 0 for ki < len(b) && b[ki] != 0 { ki++ } if ki >= len(b) { break } k := string(b[:ki]) b = b[ki+1:] vi := 0 for vi < len(b) && b[vi] != 0 { vi++ } v := string(b[:vi]) if vi < len(b) { b = b[vi+1:] } else { b = b[vi:] } h[k] = v } return h } func hasFileExtension(path string) bool { known := []string{ ".html", ".htm", ".js", ".mjs", ".css", ".wasm", ".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".otf", ".json", ".xpi", ".map", ".txt", } for _, ext := range known { el := len(ext) pl := len(path) if pl > el && path[pl-el:] == ext { return true } } return false } func hasPrefix(s, prefix string) bool { return len(s) >= len(prefix) && s[:len(prefix)] == prefix } func hasSuffix(s, suffix string) bool { return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix }