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 44 return nil, err 45 } 46 47 type fileServer struct { 48 root http.FileSystem 49 h http.Handler 50 } 51 52 // FileServer creates a wrapper around http.FileServer using the given 53 // http.FileSystem 54 // 55 // Additional http.FileSystem's can be specified and will be turned into a 56 // Handler that checks each in order, stopping at the first. 57 func FileServer(root http.FileSystem, roots ...http.FileSystem) http.Handler { 58 if len(roots) > 0 { 59 overlays := make(overlay, 1, len(roots)+1) 60 overlays[0] = root 61 overlays = append(overlays, roots...) 62 root = overlays 63 } 64 65 return FileServerWithHandler(root, http.FileServer(root)) 66 } 67 68 // FileServerWithHandler acts like FileServer, but allows a custom Handler 69 // instead of the http.FileSystem wrapped http.FileServer. 70 func FileServerWithHandler(root http.FileSystem, handler http.Handler) http.Handler { 71 return &fileServer{ 72 root, 73 handler, 74 } 75 } 76 77 func (f *fileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 78 fsh := fileserverHandler{ 79 fileServer: f, 80 w: w, 81 r: r, 82 } 83 84 if !httpencoding.HandleEncoding(r, &fsh) { 85 httpencoding.InvalidEncoding(w) 86 } 87 } 88 89 type fileserverHandler struct { 90 *fileServer 91 w http.ResponseWriter 92 r *http.Request 93 } 94 95 var detectPool = sync.Pool{ 96 New: func() interface{} { 97 return &[512]byte{} 98 }, 99 } 100 101 func (f *fileserverHandler) Handle(encoding httpencoding.Encoding) bool { 102 if encoding == "" { 103 httpencoding.ClearEncoding(f.r) 104 f.h.ServeHTTP(f.w, f.r) 105 106 return true 107 } 108 109 ext, ok := encodings[encoding] 110 if !ok { 111 return false 112 } 113 114 p := path.Clean(f.r.URL.Path) 115 116 if strings.HasSuffix(f.r.URL.Path, "/") { 117 p += "/" 118 } 119 120 m := p 121 nf, err := f.root.Open(p + ext) 122 123 if strings.HasSuffix(p, "/") { 124 m += indexPage 125 126 if err != nil { 127 nf, err = f.root.Open(m + ext) 128 p += indexPage 129 } 130 } 131 132 if err == nil { 133 ctype := mime.TypeByExtension(filepath.Ext(m)) 134 if ctype == "" { 135 if df, err := f.root.Open(m); err == nil { 136 buf := detectPool.Get().(*[512]byte) 137 n, _ := io.ReadFull(df, buf[:]) 138 ctype = http.DetectContentType(buf[:n]) 139 140 detectPool.Put(buf) 141 nf.Seek(0, io.SeekStart) 142 df.Close() 143 } 144 } 145 146 if ctype != "" { 147 if s, err := nf.Stat(); err == nil { 148 f.w.Header().Set(contentType, ctype) 149 f.w.Header().Set(contentLength, strconv.FormatInt(s.Size(), 10)) 150 f.w.Header().Set(contentEncoding, string(encoding)) 151 f.r.URL.Path = p + ext 152 153 httpencoding.ClearEncoding(f.r) 154 f.h.ServeHTTP(f.w, f.r) 155 156 return true 157 } 158 } 159 } 160 161 return false 162 } 163