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