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