// godlighty -- highly-customizable HTTP, HTTP/2, HTTPS server // Copyright (C) 2021-2024 Sergey Matveev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package godlighty import ( "bytes" "compress/gzip" "encoding/base64" "errors" "fmt" "io" "log" "net" "net/http" "net/url" "os" "path" "strconv" "strings" "sync" "time" "github.com/klauspost/compress/zstd" "go.stargrave.org/godlighty/meta4" "golang.org/x/net/webdav" ) const ( Index = "index.html" Readme = "README" ) var ( gzPool = sync.Pool{ New: func() interface{} { return gzip.NewWriter(io.Discard) }, } zstdPool = sync.Pool{ New: func() interface{} { w, err := zstd.NewWriter( io.Discard, zstd.WithEncoderLevel(zstd.SpeedDefault), ) if err != nil { log.Fatalln(err) } return w }, } MainHandler Handler ) func PathWithQuery(u *url.URL) string { if u.RawQuery == "" { return u.EscapedPath() } return u.EscapedPath() + "?" + u.RawQuery } type Handler struct{} func (h Handler) Handle( w http.ResponseWriter, r *http.Request, host string, cfg *HostCfg, ) { notFound := func() { fmt.Printf("%s %s \"%s %+q %s\" %d \"%s\"\n", r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto, http.StatusNotFound, r.Header.Get("User-Agent"), ) http.NotFound(w, r) } w.Header().Set("Server", Version) if cfg == nil { notFound() return } var username string var err error if cfg.Auth != nil { username, err = performAuth(w, r, cfg.Auth) } if username != "" { username = "user:" + username + " " } printErr := func(code int, err error) { fmt.Printf("%s %s \"%s %+q %s\" %d \"%s\" %s\"%s\"\n", r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto, code, err.Error(), username, r.Header.Get("User-Agent"), ) } switch err { case nil: break case Unauthorized: printErr(http.StatusUnauthorized, err) return default: printErr(http.StatusInternalServerError, err) http.Error(w, "internal error", http.StatusInternalServerError) return } if (cfg.ECDSATLS != nil && len(cfg.ECDSATLS.ClientCAs) > 0) || (cfg.EdDSATLS != nil && len(cfg.EdDSATLS.ClientCAs) > 0) || (cfg.GOSTTLS != nil && len(cfg.GOSTTLS.ClientCAs) > 0) { if r.TLS == nil { err = errors.New("TLS client authentication required") printErr(http.StatusForbidden, err) http.Error(w, err.Error(), http.StatusForbidden) return } else { username += r.TLS.PeerCertificates[0].Subject.String() + " " } } for _, hook := range cfg.Hooks { if done := hook(w, r); done { return } } if cfg.Root == "" { notFound() return } pthOrig := path.Clean(path.Join(cfg.Root, r.URL.Path)) pth := pthOrig fi, err := os.Stat(pth) if err != nil { notFound() return } if cfg.WebDAV && (((r.Method == http.MethodHead) && fi.IsDir()) || r.Method == http.MethodOptions || r.Method == "PROPFIND") { dav := webdav.Handler{ FileSystem: webdav.Dir(cfg.Root), LockSystem: webdav.NewMemLS(), } wc := &CountResponseWriter{ResponseWriter: w} dav.ServeHTTP(wc, r) fmt.Printf("%s %s \"WebDAV %+q\" %d %d %s\"%s\"\n", r.RemoteAddr, host, PathWithQuery(r.URL), wc.Status, wc.Size, username, r.Header.Get("User-Agent"), ) return } if !(r.Method == "" || r.Method == http.MethodGet || r.Method == http.MethodHead) { fmt.Printf("%s %s \"%s %+q %s\" %d %s\"%s\"\n", r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto, http.StatusMethodNotAllowed, username, r.Header.Get("User-Agent"), ) http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var fd *os.File var contentType string var etag string IndexLookuped: if fi.IsDir() { if cfg.DirList { entries, err := os.ReadDir(pth) if err != nil { printErr(http.StatusInternalServerError, err) http.Error(w, "internal error", http.StatusInternalServerError) return } fd, err = os.Open(pth) if err != nil { printErr(http.StatusInternalServerError, err) http.Error(w, "internal error", http.StatusInternalServerError) return } etag, err = mtimeETag(fd) fd.Close() if err != nil { printErr(http.StatusInternalServerError, err) http.Error(w, "internal error", http.StatusInternalServerError) return } var readme []byte for _, f := range append(cfg.Readmes, Readme) { readme, _ = os.ReadFile(path.Join(pth, f)) if readme != nil { break } } fd, err = dirList(cfg, r.URL.Path, pth, entries, string(readme)) if err != nil { printErr(http.StatusInternalServerError, err) http.Error(w, "internal error", http.StatusInternalServerError) return } contentType = "text/html; charset=utf-8" } else { for _, index := range append(cfg.Indices, Index) { p := path.Join(pth, index) if _, err := os.Stat(p); err == nil { pth = p fi, err = os.Stat(pth) if err != nil { notFound() return } goto IndexLookuped } } notFound() return } } if fd == nil { fd, err = os.Open(pth) if err != nil { printErr(http.StatusInternalServerError, err) http.Error(w, "internal error", http.StatusInternalServerError) return } etag, err = mtimeETag(fd) if err != nil { printErr(http.StatusInternalServerError, err) http.Error(w, "internal error", http.StatusInternalServerError) return } } defer fd.Close() if meta4fi, err := os.Stat(pth + meta4.Ext); err == nil { if meta4fi.Size() > meta4.MaxSize { goto SkipMeta4 } meta4Raw, err := os.ReadFile(pth + meta4.Ext) if err != nil { goto SkipMeta4 } base := path.Base(pth) forHTTP, err := meta4.Parse(base, meta4Raw) if err != nil { goto SkipMeta4 } w.Header().Add("Link", "<"+base+meta4.Ext+ `>; rel=describedby; type="application/metalink4+xml"`, ) for _, u := range forHTTP.URLs { w.Header().Add("Link", "<"+u+">; rel=duplicate") } for name, digest := range forHTTP.Hashes { w.Header().Add("Digest", name+"="+base64.StdEncoding.EncodeToString(digest)) } for _, u := range forHTTP.Torrents { w.Header().Add("Link", "<"+u+`>; rel=describedby; type="application/x-bittorrent"`) } } SkipMeta4: if contentType == "" { contentType = mediaType(path.Base(pth), cfg.MIMEs) } contentTypeBase := strings.SplitN(contentType, ";", 2)[0] w.Header().Set("Content-Type", contentType) if etag != "" { w.Header().Set("ETag", etag) } var wc http.ResponseWriter var bufCompressed *bytes.Buffer var gz *gzip.Writer var zstdW *zstd.Encoder if _, ok := CompressibleContentTypes[contentTypeBase]; ok { if strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") { w.Header().Set("Content-Encoding", "zstd") zstdW = zstdPool.Get().(*zstd.Encoder) defer zstdPool.Put(zstdW) bufCompressed = &bytes.Buffer{} zstdW.Reset(bufCompressed) defer zstdW.Close() wc = &gzipResponseWriter{ResponseWriter: w, Writer: zstdW} } else if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { w.Header().Set("Content-Encoding", "gzip") gz = gzPool.Get().(*gzip.Writer) defer gzPool.Put(gz) bufCompressed = &bytes.Buffer{} gz.Reset(bufCompressed) defer gz.Close() wc = &gzipResponseWriter{ResponseWriter: w, Writer: gz} } else { wc = &CountResponseWriter{ResponseWriter: w} } } else { wc = &CountResponseWriter{ResponseWriter: w} } http.ServeContent(wc, r, "", fi.ModTime().UTC().Truncate(time.Second), fd) if bufCompressed != nil { if gz != nil { gz.Close() } if zstdW != nil { zstdW.Close() } size := bufCompressed.Len() w.Header().Set("Content-Length", strconv.Itoa(size)) wr := wc.(*gzipResponseWriter) w.WriteHeader(wr.status) w.Write(bufCompressed.Bytes()) fmt.Printf("%s %s \"%s %+q %s\" %d %d %s\"%s\"\n", r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto, wr.status, size, username, r.Header.Get("User-Agent"), ) return } wr := wc.(*CountResponseWriter) fmt.Printf("%s %s \"%s %+q %s\" %d %d %s\"%s\"\n", r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto, wr.Status, wr.Size, username, r.Header.Get("User-Agent"), ) } func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if containsDotDot(r.URL.Path) { http.Error(w, "invalid URL path", http.StatusBadRequest) return } host, _, err := net.SplitHostPort(r.Host) if err != nil { host = r.Host } h.Handle(w, r, host, Hosts[host]) }