]> Sergey Matveev's repositories - godlighty.git/blob - handler.go
Built-in MIMEs compression capability
[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         if cfg == nil {
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                 return
80         }
81
82         for _, hook := range cfg.Hooks {
83                 if hook(w, r) {
84                         return
85                 }
86         }
87
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,
91                         code, err.Error(),
92                         r.Header.Get("User-Agent"),
93                 )
94         }
95
96         if cfg.Root == "" {
97                 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
98                         r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
99                         http.StatusNotFound,
100                         r.Header.Get("User-Agent"),
101                 )
102                 http.NotFound(w, r)
103                 return
104         }
105
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(),
112                 }
113                 wc := &CountResponseWriter{ResponseWriter: w}
114                 dav.ServeHTTP(wc, r)
115                 fmt.Printf("%s %s \"WebDAV %s\" %d %d \"%s\"\n",
116                         r.RemoteAddr, host, r.URL.Path,
117                         wc.Status, wc.Size,
118                         r.Header.Get("User-Agent"),
119                 )
120                 return
121         }
122
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"),
128                 )
129                 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
130                 return
131         }
132         var fd *os.File
133         var contentType string
134         var etag string
135         pth := path.Clean(path.Join(cfg.Root, r.URL.Path))
136 IndexLookup:
137         fi, err := os.Stat(pth)
138         if err != nil {
139                 fmt.Printf("%s %s \"%s %s %s\" %d \"%s\"\n",
140                         r.RemoteAddr, host, r.Method, r.URL.Path, r.Proto,
141                         http.StatusNotFound,
142                         r.Header.Get("User-Agent"),
143                 )
144                 http.NotFound(w, r)
145                 return
146         }
147         if fi.IsDir() {
148                 if cfg.DirList {
149                         entries, err := os.ReadDir(pth)
150                         if err != nil {
151                                 printErr(http.StatusInternalServerError, err)
152                                 http.Error(w, "internal error", http.StatusInternalServerError)
153                                 return
154                         }
155                         fd, err = os.Open(pth)
156                         if err != nil {
157                                 printErr(http.StatusInternalServerError, err)
158                                 http.Error(w, "internal error", http.StatusInternalServerError)
159                                 return
160                         }
161                         etag, err = ctimeETag(fd)
162                         fd.Close()
163                         if err != nil {
164                                 printErr(http.StatusInternalServerError, err)
165                                 http.Error(w, "internal error", http.StatusInternalServerError)
166                                 return
167                         }
168                         var readme []byte
169                         for _, f := range append(cfg.DirListReadmes, Readme) {
170                                 readme, _ = ioutil.ReadFile(path.Join(pth, f))
171                                 if readme != nil {
172                                         break
173                                 }
174                         }
175                         fd, err = dirList(cfg, r.URL.Path, entries, string(readme))
176                         if err != nil {
177                                 printErr(http.StatusInternalServerError, err)
178                                 http.Error(w, "internal error", http.StatusInternalServerError)
179                                 return
180                         }
181                         contentType = "text/html; charset=utf-8"
182                 } else {
183                         if cfg.Index == "" {
184                                 pth = path.Join(pth, Index)
185                         } else {
186                                 pth = path.Join(pth, cfg.Index)
187                         }
188                         goto IndexLookup
189                 }
190         }
191
192         if fd == nil {
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                 if err != nil {
201                         printErr(http.StatusInternalServerError, err)
202                         http.Error(w, "internal error", http.StatusInternalServerError)
203                         return
204                 }
205         }
206         defer fd.Close()
207
208         if _, err = os.Stat(pth + Meta4Ext); err == nil {
209                 w.Header().Set("Link", "<"+path.Base(pth)+Meta4Ext+`>; rel=describedby; type="application/metalink4+xml"`)
210         }
211
212         if contentType == "" {
213                 contentType = mediaType(path.Base(pth), cfg.MIMEOverride)
214         }
215         contentTypeBase := strings.SplitN(contentType, ";", 2)[0]
216         w.Header().Set("Content-Type", contentType)
217
218         w.Header().Set("Server", Version)
219         if etag != "" {
220                 w.Header().Set("ETag", etag)
221         }
222         var wc http.ResponseWriter
223         var bufCompressed *bytes.Buffer
224         var gz *gzip.Writer
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)
233                         defer zstdW.Close()
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)
238                         defer gzPool.Put(gz)
239                         bufCompressed = &bytes.Buffer{}
240                         gz.Reset(bufCompressed)
241                         defer gz.Close()
242                         wc = &gzipResponseWriter{ResponseWriter: w, Writer: gz}
243                 } else {
244                         wc = &CountResponseWriter{ResponseWriter: w}
245                 }
246         } else {
247                 wc = &CountResponseWriter{ResponseWriter: w}
248         }
249         http.ServeContent(wc, r, "", fi.ModTime().UTC().Truncate(time.Second), fd)
250         if bufCompressed != nil {
251                 if gz != nil {
252                         gz.Close()
253                 }
254                 if zstdW != nil {
255                         zstdW.Close()
256                 }
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,
264                         wr.status, size,
265                         r.Header.Get("User-Agent"),
266                 )
267                 return
268         }
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,
272                 wr.Status, wr.Size,
273                 r.Header.Get("User-Agent"),
274         )
275 }
276
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)
280                 return
281         }
282         host := strings.SplitN(r.Host, ":", 2)[0]
283         h.Handle(w, r, host, Hosts[host])
284 }