]> Sergey Matveev's repositories - godlighty.git/blob - handler.go
44c8a1fb40886d7aaf731ed7aeb062280f1a2fbf
[godlighty.git] / handler.go
1 /*
2 godlighty -- highly-customizable HTTP, HTTP/2, HTTPS server
3 Copyright (C) 2021-2023 Sergey Matveev <stargrave@stargrave.org>
4
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.
8
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.
13
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/>.
16 */
17
18 package godlighty
19
20 import (
21         "bytes"
22         "compress/gzip"
23         "encoding/base64"
24         "errors"
25         "fmt"
26         "io"
27         "log"
28         "net"
29         "net/http"
30         "net/url"
31         "os"
32         "path"
33         "strconv"
34         "strings"
35         "sync"
36         "time"
37
38         "github.com/klauspost/compress/zstd"
39         "go.stargrave.org/godlighty/meta4"
40         "golang.org/x/net/webdav"
41 )
42
43 const (
44         Index  = "index.html"
45         Readme = "README"
46 )
47
48 var (
49         gzPool = sync.Pool{
50                 New: func() interface{} { return gzip.NewWriter(io.Discard) },
51         }
52         zstdPool = sync.Pool{
53                 New: func() interface{} {
54                         w, err := zstd.NewWriter(
55                                 io.Discard,
56                                 zstd.WithEncoderLevel(zstd.SpeedDefault),
57                         )
58                         if err != nil {
59                                 log.Fatalln(err)
60                         }
61                         return w
62                 },
63         }
64
65         MainHandler Handler
66 )
67
68 func PathWithQuery(u *url.URL) string {
69         if u.RawQuery == "" {
70                 return u.EscapedPath()
71         }
72         return u.EscapedPath() + "?" + u.RawQuery
73 }
74
75 type Handler struct{}
76
77 func (h Handler) Handle(
78         w http.ResponseWriter, r *http.Request,
79         host string, cfg *HostCfg,
80 ) {
81         notFound := func() {
82                 fmt.Printf("%s %s \"%s %+q %s\" %d \"%s\"\n",
83                         r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
84                         http.StatusNotFound,
85                         r.Header.Get("User-Agent"),
86                 )
87                 http.NotFound(w, r)
88         }
89         w.Header().Set("Server", Version)
90         if cfg == nil {
91                 notFound()
92                 return
93         }
94
95         var username string
96         var err error
97         if cfg.Auth != nil {
98                 username, err = performAuth(w, r, cfg.Auth)
99         }
100         if username != "" {
101                 username = "user:" + username + " "
102         }
103         printErr := func(code int, err error) {
104                 fmt.Printf("%s %s \"%s %+q %s\" %d \"%s\" %s\"%s\"\n",
105                         r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
106                         code, err.Error(),
107                         username, r.Header.Get("User-Agent"),
108                 )
109         }
110         switch err {
111         case nil:
112                 break
113         case Unauthorized:
114                 printErr(http.StatusUnauthorized, err)
115                 return
116         default:
117                 printErr(http.StatusInternalServerError, err)
118                 http.Error(w, "internal error", http.StatusInternalServerError)
119                 return
120         }
121
122         if (cfg.ECDSATLS != nil && len(cfg.ECDSATLS.ClientCAs) > 0) ||
123                 (cfg.EdDSATLS != nil && len(cfg.EdDSATLS.ClientCAs) > 0) ||
124                 (cfg.GOSTTLS != nil && len(cfg.GOSTTLS.ClientCAs) > 0) {
125                 if r.TLS == nil {
126                         err = errors.New("TLS client authentication required")
127                         printErr(http.StatusForbidden, err)
128                         http.Error(w, err.Error(), http.StatusForbidden)
129                         return
130                 } else {
131                         username += r.TLS.PeerCertificates[0].Subject.String() + " "
132                 }
133         }
134
135         for _, hook := range cfg.Hooks {
136                 if done := hook(w, r); done {
137                         return
138                 }
139         }
140
141         if cfg.Root == "" {
142                 notFound()
143                 return
144         }
145
146         pthOrig := path.Clean(path.Join(cfg.Root, r.URL.Path))
147         pth := pthOrig
148         fi, err := os.Stat(pth)
149         if err != nil {
150                 notFound()
151                 return
152         }
153
154         if cfg.WebDAV && (((r.Method == http.MethodHead) && fi.IsDir()) ||
155                 r.Method == http.MethodOptions ||
156                 r.Method == "PROPFIND") {
157                 dav := webdav.Handler{
158                         FileSystem: webdav.Dir(cfg.Root),
159                         LockSystem: webdav.NewMemLS(),
160                 }
161                 wc := &CountResponseWriter{ResponseWriter: w}
162                 dav.ServeHTTP(wc, r)
163                 fmt.Printf("%s %s \"WebDAV %+q\" %d %d %s\"%s\"\n",
164                         r.RemoteAddr, host, PathWithQuery(r.URL),
165                         wc.Status, wc.Size,
166                         username, r.Header.Get("User-Agent"),
167                 )
168                 return
169         }
170
171         if !(r.Method == "" || r.Method == http.MethodGet || r.Method == http.MethodHead) {
172                 fmt.Printf("%s %s \"%s %+q %s\" %d %s\"%s\"\n",
173                         r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
174                         http.StatusMethodNotAllowed,
175                         username, r.Header.Get("User-Agent"),
176                 )
177                 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
178                 return
179         }
180
181         var fd *os.File
182         var contentType string
183         var etag string
184 IndexLookuped:
185         if fi.IsDir() {
186                 if cfg.DirList {
187                         entries, err := os.ReadDir(pth)
188                         if err != nil {
189                                 printErr(http.StatusInternalServerError, err)
190                                 http.Error(w, "internal error", http.StatusInternalServerError)
191                                 return
192                         }
193                         fd, err = os.Open(pth)
194                         if err != nil {
195                                 printErr(http.StatusInternalServerError, err)
196                                 http.Error(w, "internal error", http.StatusInternalServerError)
197                                 return
198                         }
199                         etag, err = ctimeETag(fd)
200                         fd.Close()
201                         if err != nil {
202                                 printErr(http.StatusInternalServerError, err)
203                                 http.Error(w, "internal error", http.StatusInternalServerError)
204                                 return
205                         }
206                         var readme []byte
207                         for _, f := range append(cfg.Readmes, Readme) {
208                                 readme, _ = os.ReadFile(path.Join(pth, f))
209                                 if readme != nil {
210                                         break
211                                 }
212                         }
213                         fd, err = dirList(cfg, r.URL.Path, pth, entries, string(readme))
214                         if err != nil {
215                                 printErr(http.StatusInternalServerError, err)
216                                 http.Error(w, "internal error", http.StatusInternalServerError)
217                                 return
218                         }
219                         contentType = "text/html; charset=utf-8"
220                 } else {
221                         for _, index := range append(cfg.Indices, Index) {
222                                 p := path.Join(pth, index)
223                                 if _, err := os.Stat(p); err == nil {
224                                         pth = p
225                                         fi, err = os.Stat(pth)
226                                         if err != nil {
227                                                 notFound()
228                                                 return
229                                         }
230                                         goto IndexLookuped
231                                 }
232                         }
233                         notFound()
234                         return
235                 }
236         }
237
238         if fd == nil {
239                 fd, err = os.Open(pth)
240                 if err != nil {
241                         printErr(http.StatusInternalServerError, err)
242                         http.Error(w, "internal error", http.StatusInternalServerError)
243                         return
244                 }
245                 etag, err = ctimeETag(fd)
246                 if err != nil {
247                         printErr(http.StatusInternalServerError, err)
248                         http.Error(w, "internal error", http.StatusInternalServerError)
249                         return
250                 }
251         }
252         defer fd.Close()
253
254         if meta4fi, err := os.Stat(pth + meta4.Ext); err == nil {
255                 if meta4fi.Size() > meta4.MaxSize {
256                         goto SkipMeta4
257                 }
258                 meta4Raw, err := os.ReadFile(pth + meta4.Ext)
259                 if err != nil {
260                         goto SkipMeta4
261                 }
262                 base := path.Base(pth)
263                 forHTTP, err := meta4.Parse(base, meta4Raw)
264                 if err != nil {
265                         goto SkipMeta4
266                 }
267                 w.Header().Add("Link", "<"+base+meta4.Ext+
268                         `>; rel=describedby; type="application/metalink4+xml"`,
269                 )
270                 for _, u := range forHTTP.URLs {
271                         w.Header().Add("Link", "<"+u+">; rel=duplicate")
272                 }
273                 for name, digest := range forHTTP.Hashes {
274                         w.Header().Add("Digest", name+"="+base64.StdEncoding.EncodeToString(digest))
275                 }
276         }
277 SkipMeta4:
278
279         if contentType == "" {
280                 contentType = mediaType(path.Base(pth), cfg.MIMEs)
281         }
282         contentTypeBase := strings.SplitN(contentType, ";", 2)[0]
283         w.Header().Set("Content-Type", contentType)
284
285         if etag != "" {
286                 w.Header().Set("ETag", etag)
287         }
288         var wc http.ResponseWriter
289         var bufCompressed *bytes.Buffer
290         var gz *gzip.Writer
291         var zstdW *zstd.Encoder
292         if _, ok := CompressibleContentTypes[contentTypeBase]; ok {
293                 if strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") {
294                         w.Header().Set("Content-Encoding", "zstd")
295                         zstdW = zstdPool.Get().(*zstd.Encoder)
296                         defer zstdPool.Put(zstdW)
297                         bufCompressed = &bytes.Buffer{}
298                         zstdW.Reset(bufCompressed)
299                         defer zstdW.Close()
300                         wc = &gzipResponseWriter{ResponseWriter: w, Writer: zstdW}
301                 } else if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
302                         w.Header().Set("Content-Encoding", "gzip")
303                         gz = gzPool.Get().(*gzip.Writer)
304                         defer gzPool.Put(gz)
305                         bufCompressed = &bytes.Buffer{}
306                         gz.Reset(bufCompressed)
307                         defer gz.Close()
308                         wc = &gzipResponseWriter{ResponseWriter: w, Writer: gz}
309                 } else {
310                         wc = &CountResponseWriter{ResponseWriter: w}
311                 }
312         } else {
313                 wc = &CountResponseWriter{ResponseWriter: w}
314         }
315         http.ServeContent(wc, r, "", fi.ModTime().UTC().Truncate(time.Second), fd)
316         if bufCompressed != nil {
317                 if gz != nil {
318                         gz.Close()
319                 }
320                 if zstdW != nil {
321                         zstdW.Close()
322                 }
323                 size := bufCompressed.Len()
324                 w.Header().Set("Content-Length", strconv.Itoa(size))
325                 wr := wc.(*gzipResponseWriter)
326                 w.WriteHeader(wr.status)
327                 w.Write(bufCompressed.Bytes())
328                 fmt.Printf("%s %s \"%s %+q %s\" %d %d %s\"%s\"\n",
329                         r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
330                         wr.status, size,
331                         username, r.Header.Get("User-Agent"),
332                 )
333                 return
334         }
335         wr := wc.(*CountResponseWriter)
336         fmt.Printf("%s %s \"%s %+q %s\" %d %d %s\"%s\"\n",
337                 r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
338                 wr.Status, wr.Size,
339                 username, r.Header.Get("User-Agent"),
340         )
341 }
342
343 func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
344         if containsDotDot(r.URL.Path) {
345                 http.Error(w, "invalid URL path", http.StatusBadRequest)
346                 return
347         }
348         host, _, err := net.SplitHostPort(r.Host)
349         if err != nil {
350                 host = r.Host
351         }
352         h.Handle(w, r, host, Hosts[host])
353 }