2 godlighty -- highly-customizable HTTP, HTTP/2, HTTPS server
3 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
34 "github.com/klauspost/compress/zstd"
35 "golang.org/x/net/webdav"
45 CompressibleContentTypes = make(map[string]struct{})
48 New: func() interface{} { return gzip.NewWriter(ioutil.Discard) },
51 New: func() interface{} {
52 w, err := zstd.NewWriter(
54 zstd.WithEncoderLevel(zstd.SpeedDefault),
68 func (h Handler) Handle(
69 w http.ResponseWriter, r *http.Request,
70 host string, cfg *HostCfg,
73 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
74 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
76 r.Header.Get("User-Agent"),
85 for _, hook := range cfg.Hooks {
86 if done := hook(w, r); done {
91 printErr := func(code int, err error) {
92 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\" \"%s\"\n",
93 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
95 r.Header.Get("User-Agent"),
104 if cfg.WebDAV && (r.Method == http.MethodHead ||
105 r.Method == http.MethodOptions ||
106 r.Method == "PROPFIND") {
107 dav := webdav.Handler{
108 FileSystem: webdav.Dir(cfg.Root),
109 LockSystem: webdav.NewMemLS(),
111 wc := &CountResponseWriter{ResponseWriter: w}
113 fmt.Printf("%s %s \"WebDAV %s\" %d %d \"%s\"\n",
114 r.RemoteAddr, host, r.URL.Path,
116 r.Header.Get("User-Agent"),
121 if !(r.Method == "" || r.Method == http.MethodGet) {
122 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
123 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
124 http.StatusMethodNotAllowed,
125 r.Header.Get("User-Agent"),
127 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
131 var contentType string
133 pthOrig := path.Clean(path.Join(cfg.Root, r.URL.Path))
136 fi, err := os.Stat(pth)
143 entries, err := os.ReadDir(pth)
145 printErr(http.StatusInternalServerError, err)
146 http.Error(w, "internal error", http.StatusInternalServerError)
149 fd, err = os.Open(pth)
151 printErr(http.StatusInternalServerError, err)
152 http.Error(w, "internal error", http.StatusInternalServerError)
155 etag, err = ctimeETag(fd)
158 printErr(http.StatusInternalServerError, err)
159 http.Error(w, "internal error", http.StatusInternalServerError)
163 for _, f := range append(cfg.Readmes, Readme) {
164 readme, _ = ioutil.ReadFile(path.Join(pth, f))
169 fd, err = dirList(cfg, r.URL.Path, entries, string(readme))
171 printErr(http.StatusInternalServerError, err)
172 http.Error(w, "internal error", http.StatusInternalServerError)
175 contentType = "text/html; charset=utf-8"
177 for _, index := range append(cfg.Indexes, Index) {
178 p := path.Join(pth, index)
179 if _, err := os.Stat(p); err == nil {
190 fd, err = os.Open(pth)
192 printErr(http.StatusInternalServerError, err)
193 http.Error(w, "internal error", http.StatusInternalServerError)
196 etag, err = ctimeETag(fd)
198 printErr(http.StatusInternalServerError, err)
199 http.Error(w, "internal error", http.StatusInternalServerError)
205 if _, err = os.Stat(pth + Meta4Ext); err == nil {
206 w.Header().Set("Link", "<"+path.Base(pth)+Meta4Ext+`>; rel=describedby; type="application/metalink4+xml"`)
209 if contentType == "" {
210 contentType = mediaType(path.Base(pth), cfg.MIMEs)
212 contentTypeBase := strings.SplitN(contentType, ";", 2)[0]
213 w.Header().Set("Content-Type", contentType)
215 w.Header().Set("Server", Version)
217 w.Header().Set("ETag", etag)
219 var wc http.ResponseWriter
220 var bufCompressed *bytes.Buffer
222 var zstdW *zstd.Encoder
223 if _, ok := CompressibleContentTypes[contentTypeBase]; ok {
224 if strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") {
225 w.Header().Set("Content-Encoding", "zstd")
226 zstdW = zstdPool.Get().(*zstd.Encoder)
227 defer zstdPool.Put(zstdW)
228 bufCompressed = &bytes.Buffer{}
229 zstdW.Reset(bufCompressed)
231 wc = &gzipResponseWriter{ResponseWriter: w, Writer: zstdW}
232 } else if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
233 w.Header().Set("Content-Encoding", "gzip")
234 gz = gzPool.Get().(*gzip.Writer)
236 bufCompressed = &bytes.Buffer{}
237 gz.Reset(bufCompressed)
239 wc = &gzipResponseWriter{ResponseWriter: w, Writer: gz}
241 wc = &CountResponseWriter{ResponseWriter: w}
244 wc = &CountResponseWriter{ResponseWriter: w}
246 http.ServeContent(wc, r, "", fi.ModTime().UTC().Truncate(time.Second), fd)
247 if bufCompressed != nil {
254 size := bufCompressed.Len()
255 w.Header().Set("Content-Length", strconv.Itoa(size))
256 wr := wc.(*gzipResponseWriter)
257 w.WriteHeader(wr.status)
258 w.Write(bufCompressed.Bytes())
259 fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n",
260 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
262 r.Header.Get("User-Agent"),
266 wr := wc.(*CountResponseWriter)
267 fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n",
268 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
270 r.Header.Get("User-Agent"),
274 func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
275 if containsDotDot(r.URL.Path) {
276 http.Error(w, "invalid URL path", http.StatusBadRequest)
279 host := strings.SplitN(r.Host, ":", 2)[0]
280 h.Handle(w, r, host, Hosts[host])