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