X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=handler.go;h=81bd2aa1f399101d5a10ef06619ea80cd845c9f5;hb=38ebc849c83ea919423af37b6d90c9b258925298;hp=c613d2a6b9a4c61fd39f4e52eac3c0cde47e72f6;hpb=c6d3dfd93ba38b690671e106c93d93ca5066a0f3;p=godlighty.git diff --git a/handler.go b/handler.go index c613d2a..81bd2aa 100644 --- a/handler.go +++ b/handler.go @@ -1,6 +1,6 @@ /* godlighty -- highly-customizable HTTP, HTTP/2, HTTPS server -Copyright (C) 2021 Sergey Matveev +Copyright (C) 2021-2023 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 @@ -20,10 +20,14 @@ package godlighty import ( "bytes" "compress/gzip" + "encoding/base64" + "errors" "fmt" - "io/ioutil" + "io" "log" + "net" "net/http" + "net/url" "os" "path" "strconv" @@ -32,25 +36,23 @@ import ( "time" "github.com/klauspost/compress/zstd" + "go.stargrave.org/godlighty/meta4" "golang.org/x/net/webdav" ) const ( - Index = "index.html" - Readme = "README" - Meta4Ext = ".meta4" + Index = "index.html" + Readme = "README" ) var ( - CompressibleContentTypes = make(map[string]struct{}) - gzPool = sync.Pool{ - New: func() interface{} { return gzip.NewWriter(ioutil.Discard) }, + New: func() interface{} { return gzip.NewWriter(io.Discard) }, } zstdPool = sync.Pool{ New: func() interface{} { w, err := zstd.NewWriter( - ioutil.Discard, + io.Discard, zstd.WithEncoderLevel(zstd.SpeedDefault), ) if err != nil { @@ -63,47 +65,93 @@ var ( 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, ) { - if cfg == nil { - fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n", - r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, + 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 } - for _, hook := range cfg.Hooks { - if hook(w, r) { - 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 %s %s\" %d \"%s\" \"%s\"\n", - r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, + 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(), - r.Header.Get("User-Agent"), + 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 == "" { - fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n", - r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, - http.StatusNotFound, - r.Header.Get("User-Agent"), - ) - http.NotFound(w, r) + notFound() return } - if cfg.WebDAV && (r.Method == http.MethodHead || + 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{ @@ -112,38 +160,28 @@ func (h Handler) Handle( } wc := &CountResponseWriter{ResponseWriter: w} dav.ServeHTTP(wc, r) - fmt.Printf("%s %s \"WebDAV %s\" %d %d \"%s\"\n", - r.RemoteAddr, host, r.URL.Path, + fmt.Printf("%s %s \"WebDAV %+q\" %d %d %s\"%s\"\n", + r.RemoteAddr, host, PathWithQuery(r.URL), wc.Status, wc.Size, - r.Header.Get("User-Agent"), + username, r.Header.Get("User-Agent"), ) return } - if !(r.Method == "" || r.Method == http.MethodGet) { - fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n", - r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, + 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, - r.Header.Get("User-Agent"), + 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 - pth := path.Clean(path.Join(cfg.Root, r.URL.Path)) -IndexLookup: - fi, err := os.Stat(pth) - if err != nil { - fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n", - r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, - http.StatusNotFound, - r.Header.Get("User-Agent"), - ) - http.NotFound(w, r) - return - } +IndexLookuped: if fi.IsDir() { if cfg.DirList { entries, err := os.ReadDir(pth) @@ -166,13 +204,13 @@ IndexLookup: return } var readme []byte - for _, f := range append(cfg.DirListReadmes, Readme) { - readme, _ = ioutil.ReadFile(path.Join(pth, f)) + 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, entries, string(readme)) + 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) @@ -180,12 +218,20 @@ IndexLookup: } contentType = "text/html; charset=utf-8" } else { - if cfg.Index == "" { - pth = path.Join(pth, Index) - } else { - pth = path.Join(pth, cfg.Index) + 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 + } } - goto IndexLookup + notFound() + return } } @@ -205,17 +251,40 @@ IndexLookup: } defer fd.Close() - if _, err = os.Stat(pth + Meta4Ext); err == nil { - w.Header().Set("Link", "<"+path.Base(pth)+Meta4Ext+`>; rel=describedby; type="application/metalink4+xml"`) + 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.MIMEOverride) + contentType = mediaType(path.Base(pth), cfg.MIMEs) } contentTypeBase := strings.SplitN(contentType, ";", 2)[0] w.Header().Set("Content-Type", contentType) - w.Header().Set("Server", Version) if etag != "" { w.Header().Set("ETag", etag) } @@ -259,18 +328,18 @@ IndexLookup: wr := wc.(*gzipResponseWriter) w.WriteHeader(wr.status) w.Write(bufCompressed.Bytes()) - fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n", - r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, + 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, - r.Header.Get("User-Agent"), + username, r.Header.Get("User-Agent"), ) return } wr := wc.(*CountResponseWriter) - fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n", - r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto, + 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, - r.Header.Get("User-Agent"), + username, r.Header.Get("User-Agent"), ) } @@ -279,6 +348,9 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid URL path", http.StatusBadRequest) return } - host := strings.SplitN(r.Host, ":", 2)[0] + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + host = r.Host + } h.Handle(w, r, host, Hosts[host]) }