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/>.
36 "github.com/klauspost/compress/zstd"
37 "golang.org/x/net/webdav"
48 New: func() interface{} { return gzip.NewWriter(ioutil.Discard) },
51 New: func() interface{} {
52 w, err := zstd.NewWriter(
54 zstd.WithEncoderLevel(zstd.SpeedDefault),
66 func PathWithQuery(u *url.URL) string {
68 return u.EscapedPath()
70 return u.EscapedPath() + "?" + u.RawQuery
75 func (h Handler) Handle(
76 w http.ResponseWriter, r *http.Request,
77 host string, cfg *HostCfg,
80 fmt.Printf("%s %s \"%s %+q %s\" %d \"%s\"\n",
81 r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
83 r.Header.Get("User-Agent"),
95 username, err = performAuth(w, r, cfg.Auth)
98 username = "user:" + username + " "
100 printErr := func(code int, err error) {
101 fmt.Printf("%s %s \"%s %+q %s\" %d \"%s\" %s\"%s\"\n",
102 r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
104 username, r.Header.Get("User-Agent"),
111 printErr(http.StatusUnauthorized, err)
114 printErr(http.StatusInternalServerError, err)
115 http.Error(w, "internal error", http.StatusInternalServerError)
119 if cfg.TLS != nil && len(cfg.TLS.ClientCAs) > 0 {
121 err = errors.New("TLS client authentication required")
122 printErr(http.StatusForbidden, err)
123 http.Error(w, err.Error(), http.StatusForbidden)
126 username += r.TLS.PeerCertificates[0].Subject.String() + " "
130 for _, hook := range cfg.Hooks {
131 if done := hook(w, r); done {
141 if cfg.WebDAV && (r.Method == http.MethodHead ||
142 r.Method == http.MethodOptions ||
143 r.Method == "PROPFIND") {
144 dav := webdav.Handler{
145 FileSystem: webdav.Dir(cfg.Root),
146 LockSystem: webdav.NewMemLS(),
148 wc := &CountResponseWriter{ResponseWriter: w}
150 fmt.Printf("%s %s \"WebDAV %+q\" %d %d %s\"%s\"\n",
151 r.RemoteAddr, host, PathWithQuery(r.URL),
153 username, r.Header.Get("User-Agent"),
158 if !(r.Method == "" || r.Method == http.MethodGet) {
159 fmt.Printf("%s %s \"%s %+q %s\" %d %s\"%s\"\n",
160 r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
161 http.StatusMethodNotAllowed,
162 username, r.Header.Get("User-Agent"),
164 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
168 var contentType string
170 pthOrig := path.Clean(path.Join(cfg.Root, r.URL.Path))
173 fi, err := os.Stat(pth)
180 entries, err := os.ReadDir(pth)
182 printErr(http.StatusInternalServerError, err)
183 http.Error(w, "internal error", http.StatusInternalServerError)
186 fd, err = os.Open(pth)
188 printErr(http.StatusInternalServerError, err)
189 http.Error(w, "internal error", http.StatusInternalServerError)
192 etag, err = ctimeETag(fd)
195 printErr(http.StatusInternalServerError, err)
196 http.Error(w, "internal error", http.StatusInternalServerError)
200 for _, f := range append(cfg.Readmes, Readme) {
201 readme, _ = ioutil.ReadFile(path.Join(pth, f))
206 fd, err = dirList(cfg, r.URL.Path, entries, string(readme))
208 printErr(http.StatusInternalServerError, err)
209 http.Error(w, "internal error", http.StatusInternalServerError)
212 contentType = "text/html; charset=utf-8"
214 for _, index := range append(cfg.Indexes, Index) {
215 p := path.Join(pth, index)
216 if _, err := os.Stat(p); err == nil {
227 fd, err = os.Open(pth)
229 printErr(http.StatusInternalServerError, err)
230 http.Error(w, "internal error", http.StatusInternalServerError)
233 etag, err = ctimeETag(fd)
235 printErr(http.StatusInternalServerError, err)
236 http.Error(w, "internal error", http.StatusInternalServerError)
242 if _, err = os.Stat(pth + Meta4Ext); err == nil {
243 w.Header().Set("Link", "<"+path.Base(pth)+Meta4Ext+`>; rel=describedby; type="application/metalink4+xml"`)
246 if contentType == "" {
247 contentType = mediaType(path.Base(pth), cfg.MIMEs)
249 contentTypeBase := strings.SplitN(contentType, ";", 2)[0]
250 w.Header().Set("Content-Type", contentType)
252 w.Header().Set("Server", Version)
254 w.Header().Set("ETag", etag)
256 var wc http.ResponseWriter
257 var bufCompressed *bytes.Buffer
259 var zstdW *zstd.Encoder
260 if _, ok := CompressibleContentTypes[contentTypeBase]; ok {
261 if strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") {
262 w.Header().Set("Content-Encoding", "zstd")
263 zstdW = zstdPool.Get().(*zstd.Encoder)
264 defer zstdPool.Put(zstdW)
265 bufCompressed = &bytes.Buffer{}
266 zstdW.Reset(bufCompressed)
268 wc = &gzipResponseWriter{ResponseWriter: w, Writer: zstdW}
269 } else if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
270 w.Header().Set("Content-Encoding", "gzip")
271 gz = gzPool.Get().(*gzip.Writer)
273 bufCompressed = &bytes.Buffer{}
274 gz.Reset(bufCompressed)
276 wc = &gzipResponseWriter{ResponseWriter: w, Writer: gz}
278 wc = &CountResponseWriter{ResponseWriter: w}
281 wc = &CountResponseWriter{ResponseWriter: w}
283 http.ServeContent(wc, r, "", fi.ModTime().UTC().Truncate(time.Second), fd)
284 if bufCompressed != nil {
291 size := bufCompressed.Len()
292 w.Header().Set("Content-Length", strconv.Itoa(size))
293 wr := wc.(*gzipResponseWriter)
294 w.WriteHeader(wr.status)
295 w.Write(bufCompressed.Bytes())
296 fmt.Printf("%s %s \"%s %+q %s\" %d %d %s\"%s\"\n",
297 r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
299 username, r.Header.Get("User-Agent"),
303 wr := wc.(*CountResponseWriter)
304 fmt.Printf("%s %s \"%s %+q %s\" %d %d %s\"%s\"\n",
305 r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
307 username, r.Header.Get("User-Agent"),
311 func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
312 if containsDotDot(r.URL.Path) {
313 http.Error(w, "invalid URL path", http.StatusBadRequest)
316 host := strings.SplitN(r.Host, ":", 2)[0]
317 h.Handle(w, r, host, Hosts[host])