1 // Package httpgzip is a simple wrapper around http.FileServer that looks for 2 // a compressed version of a file and serves that if the client requested 3 // compressed content 4 package httpgzip // import "vimagination.zapto.org/httpgzip" 5 6 import ( 7 "io" 8 "mime" 9 "net/http" 10 "path" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "sync" 15 16 "vimagination.zapto.org/httpencoding" 17 ) 18 19 const ( 20 contentEncoding = "Content-Encoding" 21 contentType = "Content-Type" 22 contentLength = "Content-Length" 23 indexPage = "index.html" 24 ) 25 26 var encodings = map[httpencoding.Encoding]string{ 27 "gzip": ".gz", 28 "x-gzip": ".gz", 29 "br": ".br", 30 "deflate": ".fl", 31 "zstd": ".zst", 32 } 33 34 type overlay []http.FileSystem 35 36 func (o overlay) Open(name string) (f http.File, err error) { 37 for _, fs := range o { 38 f, err = fs.Open(name) 39 if err == nil { 40 return f, nil 41 } 42 } 43 return nil, err 44 } 45 46 type fileServer struct { 47 root http.FileSystem 48 h http.Handler 49 } 50 51 // FileServer creates a wrapper around http.FileServer using the given 52 // http.FileSystem 53 // 54 // Additional http.FileSystem's can be specified and will be turned into a 55 // Handler that checks each in order, stopping at the first 56 func FileServer(root http.FileSystem, roots ...http.FileSystem) http.Handler { 57 if len(roots) > 0 { 58 overlays := make(overlay, 1, len(roots)+1) 59 overlays[0] = root 60 overlays = append(overlays, roots...) 61 root = overlays 62 } 63 return FileServerWithHandler(root, http.FileServer(root)) 64 } 65 66 // FileServerWithHandler acts like FileServer, but allows a custom Handler 67 // instead of the http.FileSystem wrapped http.FileServer 68 func FileServerWithHandler(root http.FileSystem, handler http.Handler) http.Handler { 69 return &fileServer{ 70 root, 71 handler, 72 } 73 } 74 75 func (f *fileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 76 fsh := fileserverHandler{ 77 fileServer: f, 78 w: w, 79 r: r, 80 } 81 if !httpencoding.HandleEncoding(r, &fsh) { 82 httpencoding.InvalidEncoding(w) 83 } 84 } 85 86 type fileserverHandler struct { 87 *fileServer 88 w http.ResponseWriter 89 r *http.Request 90 } 91 92 var detectPool = sync.Pool{ 93 New: func() interface{} { 94 return &[512]byte{} 95 }, 96 } 97 98 func (f *fileserverHandler) Handle(encoding httpencoding.Encoding) bool { 99 if encoding == "" { 100 httpencoding.ClearEncoding(f.r) 101 f.h.ServeHTTP(f.w, f.r) 102 return true 103 } 104 ext, ok := encodings[encoding] 105 if !ok { 106 return false 107 } 108 p := path.Clean(f.r.URL.Path) 109 if strings.HasSuffix(f.r.URL.Path, "/") { 110 p += "/" 111 } 112 m := p 113 nf, err := f.root.Open(p + ext) 114 if strings.HasSuffix(p, "/") { 115 m += indexPage 116 if err != nil { 117 nf, err = f.root.Open(m + ext) 118 p += indexPage 119 } 120 } 121 if err == nil { 122 ctype := mime.TypeByExtension(filepath.Ext(m)) 123 if ctype == "" { 124 df, err := f.root.Open(m) 125 if err == nil { 126 buf := detectPool.Get().(*[512]byte) 127 n, _ := io.ReadFull(df, buf[:]) 128 ctype = http.DetectContentType(buf[:n]) 129 detectPool.Put(buf) 130 nf.Seek(0, io.SeekStart) 131 df.Close() 132 } 133 } 134 if ctype != "" { 135 s, err := nf.Stat() 136 if err == nil { 137 f.w.Header().Set(contentType, ctype) 138 f.w.Header().Set(contentLength, strconv.FormatInt(s.Size(), 10)) 139 f.w.Header().Set(contentEncoding, string(encoding)) 140 f.r.URL.Path = p + ext 141 httpencoding.ClearEncoding(f.r) 142 f.h.ServeHTTP(f.w, f.r) 143 return true 144 } 145 } 146 } 147 return false 148 }