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