]> Sergey Matveev's repositories - godlighty.git/blob - handler.go
No CSS
[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                 for _, u := range forHTTP.Torrents {
277                         w.Header().Add("Link", "<"+u+`>; rel=describedby; type="application/x-bittorrent"`)
278                 }
279         }
280 SkipMeta4:
281
282         if contentType == "" {
283                 contentType = mediaType(path.Base(pth), cfg.MIMEs)
284         }
285         contentTypeBase := strings.SplitN(contentType, ";", 2)[0]
286         w.Header().Set("Content-Type", contentType)
287
288         if etag != "" {
289                 w.Header().Set("ETag", etag)
290         }
291         var wc http.ResponseWriter
292         var bufCompressed *bytes.Buffer
293         var gz *gzip.Writer
294         var zstdW *zstd.Encoder
295         if _, ok := CompressibleContentTypes[contentTypeBase]; ok {
296                 if strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") {
297                         w.Header().Set("Content-Encoding", "zstd")
298                         zstdW = zstdPool.Get().(*zstd.Encoder)
299                         defer zstdPool.Put(zstdW)
300                         bufCompressed = &bytes.Buffer{}
301                         zstdW.Reset(bufCompressed)
302                         defer zstdW.Close()
303                         wc = &gzipResponseWriter{ResponseWriter: w, Writer: zstdW}
304                 } else if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
305                         w.Header().Set("Content-Encoding", "gzip")
306                         gz = gzPool.Get().(*gzip.Writer)
307                         defer gzPool.Put(gz)
308                         bufCompressed = &bytes.Buffer{}
309                         gz.Reset(bufCompressed)
310                         defer gz.Close()
311                         wc = &gzipResponseWriter{ResponseWriter: w, Writer: gz}
312                 } else {
313                         wc = &CountResponseWriter{ResponseWriter: w}
314                 }
315         } else {
316                 wc = &CountResponseWriter{ResponseWriter: w}
317         }
318         http.ServeContent(wc, r, "", fi.ModTime().UTC().Truncate(time.Second), fd)
319         if bufCompressed != nil {
320                 if gz != nil {
321                         gz.Close()
322                 }
323                 if zstdW != nil {
324                         zstdW.Close()
325                 }
326                 size := bufCompressed.Len()
327                 w.Header().Set("Content-Length", strconv.Itoa(size))
328                 wr := wc.(*gzipResponseWriter)
329                 w.WriteHeader(wr.status)
330                 w.Write(bufCompressed.Bytes())
331                 fmt.Printf("%s %s \"%s %+q %s\" %d %d %s\"%s\"\n",
332                         r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
333                         wr.status, size,
334                         username, r.Header.Get("User-Agent"),
335                 )
336                 return
337         }
338         wr := wc.(*CountResponseWriter)
339         fmt.Printf("%s %s \"%s %+q %s\" %d %d %s\"%s\"\n",
340                 r.RemoteAddr, host, r.Method, PathWithQuery(r.URL), r.Proto,
341                 wr.Status, wr.Size,
342                 username, r.Header.Get("User-Agent"),
343         )
344 }
345
346 func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
347         if containsDotDot(r.URL.Path) {
348                 http.Error(w, "invalid URL path", http.StatusBadRequest)
349                 return
350         }
351         host, _, err := net.SplitHostPort(r.Host)
352         if err != nil {
353                 host = r.Host
354         }
355         h.Handle(w, r, host, Hosts[host])
356 }