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"),
82 for _, hook := range cfg.Hooks {
88 printErr := func(code int, err error) {
89 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\" \"%s\"\n",
90 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
92 r.Header.Get("User-Agent"),
97 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
98 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
100 r.Header.Get("User-Agent"),
106 if cfg.WebDAV && (r.Method == http.MethodHead ||
107 r.Method == http.MethodOptions ||
108 r.Method == "PROPFIND") {
109 dav := webdav.Handler{
110 FileSystem: webdav.Dir(cfg.Root),
111 LockSystem: webdav.NewMemLS(),
113 wc := &CountResponseWriter{ResponseWriter: w}
115 fmt.Printf("%s %s \"WebDAV %s\" %d %d \"%s\"\n",
116 r.RemoteAddr, host, r.URL.Path,
118 r.Header.Get("User-Agent"),
123 if !(r.Method == "" || r.Method == http.MethodGet) {
124 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
125 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
126 http.StatusMethodNotAllowed,
127 r.Header.Get("User-Agent"),
129 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
133 var contentType string
135 pth := path.Clean(path.Join(cfg.Root, r.URL.Path))
137 fi, err := os.Stat(pth)
139 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
140 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
142 r.Header.Get("User-Agent"),
149 entries, err := os.ReadDir(pth)
151 printErr(http.StatusInternalServerError, err)
152 http.Error(w, "internal error", http.StatusInternalServerError)
155 fd, err = os.Open(pth)
157 printErr(http.StatusInternalServerError, err)
158 http.Error(w, "internal error", http.StatusInternalServerError)
161 etag, err = ctimeETag(fd)
164 printErr(http.StatusInternalServerError, err)
165 http.Error(w, "internal error", http.StatusInternalServerError)
169 for _, f := range append(cfg.DirListReadmes, Readme) {
170 readme, _ = ioutil.ReadFile(path.Join(pth, f))
175 fd, err = dirList(cfg, r.URL.Path, entries, string(readme))
177 printErr(http.StatusInternalServerError, err)
178 http.Error(w, "internal error", http.StatusInternalServerError)
181 contentType = "text/html; charset=utf-8"
184 pth = path.Join(pth, Index)
186 pth = path.Join(pth, cfg.Index)
193 fd, err = os.Open(pth)
195 printErr(http.StatusInternalServerError, err)
196 http.Error(w, "internal error", http.StatusInternalServerError)
199 etag, err = ctimeETag(fd)
201 printErr(http.StatusInternalServerError, err)
202 http.Error(w, "internal error", http.StatusInternalServerError)
208 if _, err = os.Stat(pth + Meta4Ext); err == nil {
209 w.Header().Set("Link", "<"+path.Base(pth)+Meta4Ext+`>; rel=describedby; type="application/metalink4+xml"`)
212 if contentType == "" {
213 contentType = mediaType(path.Base(pth), cfg.MIMEOverride)
215 contentTypeBase := strings.SplitN(contentType, ";", 2)[0]
216 w.Header().Set("Content-Type", contentType)
218 w.Header().Set("Server", Version)
220 w.Header().Set("ETag", etag)
222 var wc http.ResponseWriter
223 var bufCompressed *bytes.Buffer
225 var zstdW *zstd.Encoder
226 if _, ok := CompressibleContentTypes[contentTypeBase]; ok {
227 if strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") {
228 w.Header().Set("Content-Encoding", "zstd")
229 zstdW = zstdPool.Get().(*zstd.Encoder)
230 defer zstdPool.Put(zstdW)
231 bufCompressed = &bytes.Buffer{}
232 zstdW.Reset(bufCompressed)
234 wc = &gzipResponseWriter{ResponseWriter: w, Writer: zstdW}
235 } else if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
236 w.Header().Set("Content-Encoding", "gzip")
237 gz = gzPool.Get().(*gzip.Writer)
239 bufCompressed = &bytes.Buffer{}
240 gz.Reset(bufCompressed)
242 wc = &gzipResponseWriter{ResponseWriter: w, Writer: gz}
244 wc = &CountResponseWriter{ResponseWriter: w}
247 wc = &CountResponseWriter{ResponseWriter: w}
249 http.ServeContent(wc, r, "", fi.ModTime().UTC().Truncate(time.Second), fd)
250 if bufCompressed != nil {
257 size := bufCompressed.Len()
258 w.Header().Set("Content-Length", strconv.Itoa(size))
259 wr := wc.(*gzipResponseWriter)
260 w.WriteHeader(wr.status)
261 w.Write(bufCompressed.Bytes())
262 fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n",
263 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
265 r.Header.Get("User-Agent"),
269 wr := wc.(*CountResponseWriter)
270 fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n",
271 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
273 r.Header.Get("User-Agent"),
277 func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
278 if containsDotDot(r.URL.Path) {
279 http.Error(w, "invalid URL path", http.StatusBadRequest)
282 host := strings.SplitN(r.Host, ":", 2)[0]
283 h.Handle(w, r, host, Hosts[host])