]> Sergey Matveev's repositories - godlighty.git/blob - handler.go
Move domain-specific things to separate directory
[godlighty.git] / handler.go
1 /*
2 godlighty -- highly-customizable HTTP, HTTP/2, HTTPS server
3 Copyright (C) 2021 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         "fmt"
24         "io/ioutil"
25         "log"
26         "net/http"
27         "os"
28         "path"
29         "strconv"
30         "strings"
31         "sync"
32         "time"
33
34         "github.com/klauspost/compress/zstd"
35         "golang.org/x/net/webdav"
36 )
37
38 const (
39         Index    = "index.html"
40         Readme   = "README"
41         Meta4Ext = ".meta4"
42 )
43
44 var (
45         CompressibleContentTypes = make(map[string]struct{})
46
47         gzPool = sync.Pool{
48                 New: func() interface{} { return gzip.NewWriter(ioutil.Discard) },
49         }
50         zstdPool = sync.Pool{
51                 New: func() interface{} {
52                         w, err := zstd.NewWriter(
53                                 ioutil.Discard,
54                                 zstd.WithEncoderLevel(zstd.SpeedDefault),
55                         )
56                         if err != nil {
57                                 log.Fatalln(err)
58                         }
59                         return w
60                 },
61         }
62
63         MainHandler Handler
64 )
65
66 type Handler struct{}
67
68 func (h Handler) Handle(
69         w http.ResponseWriter, r *http.Request,
70         host string, cfg *HostCfg,
71 ) {
72         notFound := func() {
73                 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
74                         r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
75                         http.StatusNotFound,
76                         r.Header.Get("User-Agent"),
77                 )
78                 http.NotFound(w, r)
79         }
80         if cfg == nil {
81                 notFound()
82                 return
83         }
84
85         for _, hook := range cfg.Hooks {
86                 if done := hook(w, r); done {
87                         return
88                 }
89         }
90
91         printErr := func(code int, err error) {
92                 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\" \"%s\"\n",
93                         r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
94                         code, err.Error(),
95                         r.Header.Get("User-Agent"),
96                 )
97         }
98
99         if cfg.Root == "" {
100                 notFound()
101                 return
102         }
103
104         if cfg.WebDAV && (r.Method == http.MethodHead ||
105                 r.Method == http.MethodOptions ||
106                 r.Method == "PROPFIND") {
107                 dav := webdav.Handler{
108                         FileSystem: webdav.Dir(cfg.Root),
109                         LockSystem: webdav.NewMemLS(),
110                 }
111                 wc := &CountResponseWriter{ResponseWriter: w}
112                 dav.ServeHTTP(wc, r)
113                 fmt.Printf("%s %s \"WebDAV %s\" %d %d \"%s\"\n",
114                         r.RemoteAddr, host, r.URL.Path,
115                         wc.Status, wc.Size,
116                         r.Header.Get("User-Agent"),
117                 )
118                 return
119         }
120
121         if !(r.Method == "" || r.Method == http.MethodGet) {
122                 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
123                         r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
124                         http.StatusMethodNotAllowed,
125                         r.Header.Get("User-Agent"),
126                 )
127                 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
128                 return
129         }
130         var fd *os.File
131         var contentType string
132         var etag string
133         pthOrig := path.Clean(path.Join(cfg.Root, r.URL.Path))
134         pth := pthOrig
135 IndexLookup:
136         fi, err := os.Stat(pth)
137         if err != nil {
138                 notFound()
139                 return
140         }
141         if fi.IsDir() {
142                 if cfg.DirList {
143                         entries, err := os.ReadDir(pth)
144                         if err != nil {
145                                 printErr(http.StatusInternalServerError, err)
146                                 http.Error(w, "internal error", http.StatusInternalServerError)
147                                 return
148                         }
149                         fd, err = os.Open(pth)
150                         if err != nil {
151                                 printErr(http.StatusInternalServerError, err)
152                                 http.Error(w, "internal error", http.StatusInternalServerError)
153                                 return
154                         }
155                         etag, err = ctimeETag(fd)
156                         fd.Close()
157                         if err != nil {
158                                 printErr(http.StatusInternalServerError, err)
159                                 http.Error(w, "internal error", http.StatusInternalServerError)
160                                 return
161                         }
162                         var readme []byte
163                         for _, f := range append(cfg.Readmes, Readme) {
164                                 readme, _ = ioutil.ReadFile(path.Join(pth, f))
165                                 if readme != nil {
166                                         break
167                                 }
168                         }
169                         fd, err = dirList(cfg, r.URL.Path, entries, string(readme))
170                         if err != nil {
171                                 printErr(http.StatusInternalServerError, err)
172                                 http.Error(w, "internal error", http.StatusInternalServerError)
173                                 return
174                         }
175                         contentType = "text/html; charset=utf-8"
176                 } else {
177                         for _, index := range append(cfg.Indexes, Index) {
178                                 p := path.Join(pth, index)
179                                 if _, err := os.Stat(p); err == nil {
180                                         pth = p
181                                         goto IndexLookup
182                                 }
183                         }
184                         notFound()
185                         return
186                 }
187         }
188
189         if fd == nil {
190                 fd, err = os.Open(pth)
191                 if err != nil {
192                         printErr(http.StatusInternalServerError, err)
193                         http.Error(w, "internal error", http.StatusInternalServerError)
194                         return
195                 }
196                 etag, err = ctimeETag(fd)
197                 if err != nil {
198                         printErr(http.StatusInternalServerError, err)
199                         http.Error(w, "internal error", http.StatusInternalServerError)
200                         return
201                 }
202         }
203         defer fd.Close()
204
205         if _, err = os.Stat(pth + Meta4Ext); err == nil {
206                 w.Header().Set("Link", "<"+path.Base(pth)+Meta4Ext+`>; rel=describedby; type="application/metalink4+xml"`)
207         }
208
209         if contentType == "" {
210                 contentType = mediaType(path.Base(pth), cfg.MIMEs)
211         }
212         contentTypeBase := strings.SplitN(contentType, ";", 2)[0]
213         w.Header().Set("Content-Type", contentType)
214
215         w.Header().Set("Server", Version)
216         if etag != "" {
217                 w.Header().Set("ETag", etag)
218         }
219         var wc http.ResponseWriter
220         var bufCompressed *bytes.Buffer
221         var gz *gzip.Writer
222         var zstdW *zstd.Encoder
223         if _, ok := CompressibleContentTypes[contentTypeBase]; ok {
224                 if strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") {
225                         w.Header().Set("Content-Encoding", "zstd")
226                         zstdW = zstdPool.Get().(*zstd.Encoder)
227                         defer zstdPool.Put(zstdW)
228                         bufCompressed = &bytes.Buffer{}
229                         zstdW.Reset(bufCompressed)
230                         defer zstdW.Close()
231                         wc = &gzipResponseWriter{ResponseWriter: w, Writer: zstdW}
232                 } else if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
233                         w.Header().Set("Content-Encoding", "gzip")
234                         gz = gzPool.Get().(*gzip.Writer)
235                         defer gzPool.Put(gz)
236                         bufCompressed = &bytes.Buffer{}
237                         gz.Reset(bufCompressed)
238                         defer gz.Close()
239                         wc = &gzipResponseWriter{ResponseWriter: w, Writer: gz}
240                 } else {
241                         wc = &CountResponseWriter{ResponseWriter: w}
242                 }
243         } else {
244                 wc = &CountResponseWriter{ResponseWriter: w}
245         }
246         http.ServeContent(wc, r, "", fi.ModTime().UTC().Truncate(time.Second), fd)
247         if bufCompressed != nil {
248                 if gz != nil {
249                         gz.Close()
250                 }
251                 if zstdW != nil {
252                         zstdW.Close()
253                 }
254                 size := bufCompressed.Len()
255                 w.Header().Set("Content-Length", strconv.Itoa(size))
256                 wr := wc.(*gzipResponseWriter)
257                 w.WriteHeader(wr.status)
258                 w.Write(bufCompressed.Bytes())
259                 fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n",
260                         r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
261                         wr.status, size,
262                         r.Header.Get("User-Agent"),
263                 )
264                 return
265         }
266         wr := wc.(*CountResponseWriter)
267         fmt.Printf("%s %s \"%s %s %s\" %d %d \"%s\"\n",
268                 r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
269                 wr.Status, wr.Size,
270                 r.Header.Get("User-Agent"),
271         )
272 }
273
274 func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
275         if containsDotDot(r.URL.Path) {
276                 http.Error(w, "invalid URL path", http.StatusBadRequest)
277                 return
278         }
279         host := strings.SplitN(r.Host, ":", 2)[0]
280         h.Handle(w, r, host, Hosts[host])
281 }