static.mx raw
1 package static
2
3 import (
4 "net/url"
5 "os"
6 )
7
8 // Serve returns status, headers, body for a static file request.
9 // dir is the static root directory. path is the URL path from the request.
10 func Serve(dir, path string) (int, map[string]string, []byte) {
11 if path == "" || path == "/" {
12 path = "/index.html"
13 }
14 if decoded, err := url.PathUnescape(path); err == nil {
15 path = decoded
16 }
17 data, err := os.ReadFile(dir | path)
18 if err != nil {
19 if hasFileExtension(path) {
20 return 404, map[string]string{"Content-Type": "text/plain"}, []byte("404 not found\n")
21 }
22 data, err = os.ReadFile(dir | "/index.html")
23 if err != nil {
24 return 404, map[string]string{"Content-Type": "text/plain"}, []byte("404 not found\n")
25 }
26 return 200, map[string]string{
27 "Content-Type": "text/html; charset=utf-8",
28 "Cache-Control": "no-cache",
29 "Cross-Origin-Opener-Policy": "same-origin",
30 "Cross-Origin-Embedder-Policy": "require-corp",
31 }, data
32 }
33 ct := "application/octet-stream"
34 switch {
35 case hasSuffix(path, ".html"):
36 ct = "text/html; charset=utf-8"
37 case hasSuffix(path, ".js"), hasSuffix(path, ".mjs"):
38 ct = "application/javascript"
39 case hasSuffix(path, ".css"):
40 ct = "text/css"
41 case hasSuffix(path, ".json"):
42 ct = "application/json"
43 case hasSuffix(path, ".svg"):
44 ct = "image/svg+xml"
45 case hasSuffix(path, ".png"):
46 ct = "image/png"
47 case hasSuffix(path, ".ico"):
48 ct = "image/x-icon"
49 case hasSuffix(path, ".wasm"):
50 ct = "application/wasm"
51 case hasSuffix(path, ".webp"):
52 ct = "image/webp"
53 case hasSuffix(path, ".woff2"):
54 ct = "font/woff2"
55 case hasSuffix(path, ".xpi"):
56 ct = "application/x-xpinstall"
57 }
58 h := map[string]string{
59 "Content-Type": ct,
60 "Cross-Origin-Opener-Policy": "same-origin",
61 "Cross-Origin-Embedder-Policy": "require-corp",
62 "Cross-Origin-Resource-Policy": "same-origin",
63 "Cache-Control": "no-cache",
64 }
65 if path == "/$sw/wasm-host-sw.mjs" {
66 h["Service-Worker-Allowed"] = "/"
67 }
68 return 200, h, data
69 }
70
71 // EncodeHeaders serializes map[string]string as null-separated key\0val\0 pairs.
72 func EncodeHeaders(h map[string]string) []byte {
73 sz := 0
74 for k, v := range h {
75 sz += len(k) + 1 + len(v) + 1
76 }
77 out := []byte{:0:sz}
78 for k, v := range h {
79 out = append(out, k...)
80 out = append(out, 0)
81 out = append(out, v...)
82 out = append(out, 0)
83 }
84 return out
85 }
86
87 // DecodeHeaders deserializes null-separated key\0val\0 pairs to map.
88 func DecodeHeaders(b []byte) map[string]string {
89 h := map[string]string{}
90 for len(b) > 0 {
91 ki := 0
92 for ki < len(b) && b[ki] != 0 {
93 ki++
94 }
95 if ki >= len(b) {
96 break
97 }
98 k := string(b[:ki])
99 b = b[ki+1:]
100 vi := 0
101 for vi < len(b) && b[vi] != 0 {
102 vi++
103 }
104 v := string(b[:vi])
105 if vi < len(b) {
106 b = b[vi+1:]
107 } else {
108 b = b[vi:]
109 }
110 h[k] = v
111 }
112 return h
113 }
114
115 func hasFileExtension(path string) bool {
116 known := []string{
117 ".html", ".htm", ".js", ".mjs", ".css", ".wasm", ".svg",
118 ".png", ".jpg", ".jpeg", ".webp", ".gif", ".ico",
119 ".woff", ".woff2", ".ttf", ".otf", ".json", ".xpi", ".map", ".txt",
120 }
121 for _, ext := range known {
122 el := len(ext)
123 pl := len(path)
124 if pl > el && path[pl-el:] == ext {
125 return true
126 }
127 }
128 return false
129 }
130
131 func hasPrefix(s, prefix string) bool {
132 return len(s) >= len(prefix) && s[:len(prefix)] == prefix
133 }
134
135 func hasSuffix(s, suffix string) bool {
136 return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
137 }
138