1package http
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "crypto/sha1"
8 "crypto/tls"
9 "encoding/base64"
10 "errors"
11 "fmt"
12 htmltemplate "html/template"
13 "io"
14 "io/fs"
15 golog "log"
16 "log/slog"
17 "net"
18 "net/http"
19 "net/http/httputil"
20 "net/textproto"
21 "net/url"
22 "os"
23 "path/filepath"
24 "sort"
25 "strings"
26 "syscall"
27 "time"
28
29 "github.com/mjl-/mox/config"
30 "github.com/mjl-/mox/dns"
31 "github.com/mjl-/mox/mlog"
32 "github.com/mjl-/mox/mox-"
33 "github.com/mjl-/mox/moxio"
34)
35
36func recvid(r *http.Request) string {
37 cid := mox.CidFromCtx(r.Context())
38 if cid <= 0 {
39 return ""
40 }
41 return " (id " + mox.ReceivedID(cid) + ")"
42}
43
44// WebHandle serves an HTTP request by going through the list of WebHandlers,
45// check if there is a domain+path match, and running the handler if so.
46// WebHandle runs after the built-in handlers for mta-sts, autoconfig, etc.
47// If no handler matched, false is returned.
48// WebHandle sets w.Name to that of the matching handler.
49func WebHandle(w *loggingWriter, r *http.Request, host dns.IPDomain) (handled bool) {
50 conf := mox.Conf.DynamicConfig()
51 redirects := conf.WebDNSDomainRedirects
52 handlers := conf.WebHandlers
53
54 for from, to := range redirects {
55 if host.Domain != from {
56 continue
57 }
58 u := r.URL
59 u.Scheme = "https"
60 u.Host = to.Name()
61 w.Handler = "(domainredirect)"
62 http.Redirect(w, r, u.String(), http.StatusPermanentRedirect)
63 return true
64 }
65
66 for _, h := range handlers {
67 if host.Domain != h.DNSDomain {
68 continue
69 }
70 loc := h.Path.FindStringIndex(r.URL.Path)
71 if loc == nil {
72 continue
73 }
74 s := loc[0]
75 e := loc[1]
76 path := r.URL.Path[s:e]
77
78 if r.TLS == nil && !h.DontRedirectPlainHTTP {
79 u := *r.URL
80 u.Scheme = "https"
81 u.Host = h.DNSDomain.Name()
82 w.Handler = h.Name
83 w.Compress = h.Compress
84 http.Redirect(w, r, u.String(), http.StatusPermanentRedirect)
85 return true
86 }
87
88 // We don't want the loggingWriter to override the static handler's decisions to compress.
89 w.Compress = h.Compress
90 if h.WebStatic != nil && HandleStatic(h.WebStatic, h.Compress, w, r) {
91 w.Handler = h.Name
92 return true
93 }
94 if h.WebRedirect != nil && HandleRedirect(h.WebRedirect, w, r) {
95 w.Handler = h.Name
96 return true
97 }
98 if h.WebForward != nil && HandleForward(h.WebForward, w, r, path) {
99 w.Handler = h.Name
100 return true
101 }
102 if h.WebInternal != nil && HandleInternal(h.WebInternal, w, r) {
103 w.Handler = h.Name
104 return true
105 }
106 }
107 w.Compress = false
108 return false
109}
110
111var lsTemplate = htmltemplate.Must(htmltemplate.New("ls").Parse(`<!doctype html>
112<html>
113 <head>
114 <meta charset="utf-8" />
115 <meta name="viewport" content="width=device-width, initial-scale=1" />
116 <title>ls</title>
117 <style>
118body, html { padding: 1em; font-size: 16px; }
119* { font-size: inherit; font-family: ubuntu, lato, sans-serif; margin: 0; padding: 0; box-sizing: border-box; }
120h1 { margin-bottom: 1ex; font-size: 1.2rem; }
121table td, table th { padding: .2em .5em; }
122table > tbody > tr:nth-child(odd) { background-color: #f8f8f8; }
123[title] { text-decoration: underline; text-decoration-style: dotted; }
124 </style>
125 </head>
126 <body>
127 <h1>ls</h1>
128 <table>
129 <thead>
130 <tr>
131 <th>Size in MB</th>
132 <th>Modified (UTC)</th>
133 <th>Name</th>
134 </tr>
135 </thead>
136 <tbody>
137 {{ if not .Files }}
138 <tr><td colspan="3">No files.</td></tr>
139 {{ end }}
140 {{ range .Files }}
141 <tr>
142 <td title="{{ .Size }} bytes" style="text-align: right">{{ .SizeReadable }}{{ if .SizePad }}<span style="visibility:hidden">.  </span>{{ end }}</td>
143 <td>{{ .Modified }}</td>
144 <td><a style="display: block" href="{{ .Name }}">{{ .Name }}</a></td>
145 </tr>
146 {{ end }}
147 </tbody>
148 </table>
149 </body>
150</html>
151`))
152
153// HandleStatic serves static files. If a directory is requested and the URL
154// path doesn't end with a slash, a response with a redirect to the URL path with trailing
155// slash is written. If a directory is requested and an index.html exists, that
156// file is returned. Otherwise, for directories with ListFiles configured, a
157// directory listing is returned.
158func HandleStatic(h *config.WebStatic, compress bool, w http.ResponseWriter, r *http.Request) (handled bool) {
159 log := func() mlog.Log {
160 return pkglog.WithContext(r.Context())
161 }
162 if r.Method != "GET" && r.Method != "HEAD" {
163 if h.ContinueNotFound {
164 // Give another handler that is presumbly configured, for the same path, a chance.
165 // E.g. an app that may generate this file for future requests to pick up.
166 return false
167 }
168 http.Error(w, "405 - method not allowed", http.StatusMethodNotAllowed)
169 return true
170 }
171
172 var fspath string
173 if h.StripPrefix != "" {
174 if !strings.HasPrefix(r.URL.Path, h.StripPrefix) {
175 if h.ContinueNotFound {
176 // We haven't handled this request, try a next WebHandler in the list.
177 return false
178 }
179 http.NotFound(w, r)
180 return true
181 }
182 fspath = filepath.Join(h.Root, strings.TrimPrefix(r.URL.Path, h.StripPrefix))
183 } else {
184 fspath = filepath.Join(h.Root, r.URL.Path)
185 }
186 // fspath will not have a trailing slash anymore, we'll correct for it
187 // later when the path turns out to be file instead of a directory.
188
189 serveFile := func(name string, fi fs.FileInfo, content *os.File) {
190 // ServeContent only sets a content-type if not already present in the response headers.
191 hdr := w.Header()
192 for k, v := range h.ResponseHeaders {
193 hdr.Add(k, v)
194 }
195 // We transparently compress here, but still use ServeContent, because it handles
196 // conditional requests, range requests. It's a bit of a hack, but on first write
197 // to staticgzcacheReplacer where we are compressing, we write the full compressed
198 // file instead, and return an error to ServeContent so it stops. We still have all
199 // the useful behaviour (status code and headers) from ServeContent.
200 xw := w
201 if compress && acceptsGzip(r) && compressibleContent(content) {
202 xw = &staticgzcacheReplacer{w, r, content.Name(), content, fi.ModTime(), fi.Size(), 0, false}
203 } else {
204 w.(*loggingWriter).Compress = false
205 }
206 http.ServeContent(xw, r, name, fi.ModTime(), content)
207 }
208
209 f, err := os.Open(fspath)
210 if err != nil {
211 if os.IsNotExist(err) || errors.Is(err, syscall.ENOTDIR) {
212 if h.ContinueNotFound {
213 // We haven't handled this request, try a next WebHandler in the list.
214 return false
215 }
216 http.NotFound(w, r)
217 return true
218 } else if os.IsPermission(err) {
219 // If we tried opening a directory, we may not have permission to read it, but
220 // still access files inside it (execute bit), such as index.html. So try to serve it.
221 index, err := os.Open(filepath.Join(fspath, "index.html"))
222 if err == nil {
223 defer index.Close()
224 var ifi os.FileInfo
225 ifi, err = index.Stat()
226 if err != nil {
227 log().Errorx("stat index.html in directory we cannot list", err, slog.Any("url", r.URL), slog.String("fspath", fspath))
228 http.Error(w, "500 - internal server error"+recvid(r), http.StatusInternalServerError)
229 return true
230 }
231 w.Header().Set("Content-Type", "text/html; charset=utf-8")
232 serveFile("index.html", ifi, index)
233 return true
234 }
235 http.Error(w, "403 - permission denied", http.StatusForbidden)
236 return true
237 }
238 log().Errorx("open file for static file serving", err, slog.Any("url", r.URL), slog.String("fspath", fspath))
239 http.Error(w, "500 - internal server error"+recvid(r), http.StatusInternalServerError)
240 return true
241 }
242 defer f.Close()
243
244 fi, err := f.Stat()
245 if err != nil {
246 log().Errorx("stat file for static file serving", err, slog.Any("url", r.URL), slog.String("fspath", fspath))
247 http.Error(w, "500 - internal server error"+recvid(r), http.StatusInternalServerError)
248 return true
249 }
250 // Redirect if the local path is a directory.
251 if fi.IsDir() && !strings.HasSuffix(r.URL.Path, "/") {
252 http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
253 return true
254 } else if !fi.IsDir() && strings.HasSuffix(r.URL.Path, "/") {
255 if h.ContinueNotFound {
256 return false
257 }
258 http.NotFound(w, r)
259 return true
260 }
261
262 if fi.IsDir() {
263 index, err := os.Open(filepath.Join(fspath, "index.html"))
264 if err != nil && os.IsPermission(err) {
265 http.Error(w, "403 - permission denied", http.StatusForbidden)
266 return true
267 } else if err != nil && os.IsNotExist(err) && !h.ListFiles {
268 if h.ContinueNotFound {
269 return false
270 }
271 http.Error(w, "403 - permission denied", http.StatusForbidden)
272 return true
273 } else if err == nil {
274 defer index.Close()
275 var ifi os.FileInfo
276 ifi, err = index.Stat()
277 if err == nil {
278 w.Header().Set("Content-Type", "text/html; charset=utf-8")
279 serveFile("index.html", ifi, index)
280 return true
281 }
282 }
283 if !os.IsNotExist(err) {
284 log().Errorx("stat for static file serving", err, slog.Any("url", r.URL), slog.String("fspath", fspath))
285 http.Error(w, "500 - internal server error"+recvid(r), http.StatusInternalServerError)
286 return true
287 }
288
289 type File struct {
290 Name string
291 Size int64
292 SizeReadable string
293 SizePad bool // Whether the size needs padding because it has no decimal point.
294 Modified string
295 }
296 files := []File{}
297 if r.URL.Path != "/" {
298 files = append(files, File{"..", 0, "", false, ""})
299 }
300 for {
301 l, err := f.Readdir(1000)
302 for _, e := range l {
303 mb := float64(e.Size()) / (1024 * 1024)
304 var size string
305 var sizepad bool
306 if !e.IsDir() {
307 if mb >= 10 {
308 size = fmt.Sprintf("%d", int64(mb))
309 sizepad = true
310 } else {
311 size = fmt.Sprintf("%.2f", mb)
312 }
313 }
314 const dateTime = "2006-01-02 15:04:05" // time.DateTime, but only since go1.20.
315 modified := e.ModTime().UTC().Format(dateTime)
316 f := File{e.Name(), e.Size(), size, sizepad, modified}
317 if e.IsDir() {
318 f.Name += "/"
319 }
320 files = append(files, f)
321 }
322 if err == io.EOF {
323 break
324 } else if err != nil {
325 log().Errorx("reading directory for file listing", err, slog.Any("url", r.URL), slog.String("fspath", fspath))
326 http.Error(w, "500 - internal server error"+recvid(r), http.StatusInternalServerError)
327 return true
328 }
329 }
330 sort.Slice(files, func(i, j int) bool {
331 return files[i].Name < files[j].Name
332 })
333 hdr := w.Header()
334 hdr.Set("Content-Type", "text/html; charset=utf-8")
335 for k, v := range h.ResponseHeaders {
336 if !strings.EqualFold(k, "content-type") {
337 hdr.Add(k, v)
338 }
339 }
340 err = lsTemplate.Execute(w, map[string]any{"Files": files})
341 if err != nil && !moxio.IsClosed(err) {
342 log().Errorx("executing directory listing template", err)
343 }
344 return true
345 }
346
347 serveFile(fspath, fi, f)
348 return true
349}
350
351// HandleRedirect writes a response with an HTTP redirect.
352func HandleRedirect(h *config.WebRedirect, w http.ResponseWriter, r *http.Request) (handled bool) {
353 var dstpath string
354 if h.OrigPath == nil {
355 // No path rewrite necessary.
356 dstpath = r.URL.Path
357 } else if !h.OrigPath.MatchString(r.URL.Path) {
358 http.NotFound(w, r)
359 return true
360 } else {
361 dstpath = h.OrigPath.ReplaceAllString(r.URL.Path, h.ReplacePath)
362 }
363
364 u := *r.URL
365 u.Opaque = ""
366 u.RawPath = ""
367 u.OmitHost = false
368 if h.URL != nil {
369 u.Scheme = h.URL.Scheme
370 u.Host = h.URL.Host
371 u.ForceQuery = h.URL.ForceQuery
372 u.RawQuery = h.URL.RawQuery
373 u.Fragment = h.URL.Fragment
374 if r.URL.RawQuery != "" {
375 if u.RawQuery != "" {
376 u.RawQuery += "&"
377 }
378 u.RawQuery += r.URL.RawQuery
379 }
380 }
381 u.Path = dstpath
382 code := http.StatusPermanentRedirect
383 if h.StatusCode != 0 {
384 code = h.StatusCode
385 }
386
387 // If we would be redirecting to the same scheme,host,path, we would get here again
388 // causing a redirect loop. Instead, this causes this redirect to not match,
389 // allowing to try the next WebHandler. This can be used to redirect all plain http
390 // requests to https.
391 reqscheme := "http"
392 if r.TLS != nil {
393 reqscheme = "https"
394 }
395 if reqscheme == u.Scheme && r.Host == u.Host && r.URL.Path == u.Path {
396 return false
397 }
398
399 http.Redirect(w, r, u.String(), code)
400 return true
401}
402
403// HandleInternal passes the request to an internal service.
404func HandleInternal(h *config.WebInternal, w http.ResponseWriter, r *http.Request) (handled bool) {
405 h.Handler.ServeHTTP(w, r)
406 return true
407}
408
409// HandleForward handles a request by forwarding it to another webserver and
410// passing the response on. I.e. a reverse proxy. It handles websocket
411// connections by monitoring the websocket handshake and then just passing along the
412// websocket frames.
413func HandleForward(h *config.WebForward, w http.ResponseWriter, r *http.Request, path string) (handled bool) {
414 log := func() mlog.Log {
415 return pkglog.WithContext(r.Context())
416 }
417
418 xr := *r
419 r = &xr
420 if h.StripPath {
421 u := *r.URL
422 u.Path = r.URL.Path[len(path):]
423 if !strings.HasPrefix(u.Path, "/") {
424 u.Path = "/" + u.Path
425 }
426 u.RawPath = ""
427 r.URL = &u
428 }
429
430 // Remove any forwarded headers passed in by client.
431 hdr := http.Header{}
432 for k, vl := range r.Header {
433 if k == "Forwarded" || k == "X-Forwarded" || strings.HasPrefix(k, "X-Forwarded-") {
434 continue
435 }
436 hdr[k] = vl
437 }
438 r.Header = hdr
439
440 // Add our own X-Forwarded headers. ReverseProxy will add X-Forwarded-For.
441 r.Header["X-Forwarded-Host"] = []string{r.Host}
442 proto := "http"
443 if r.TLS != nil {
444 proto = "https"
445 }
446 r.Header["X-Forwarded-Proto"] = []string{proto}
447 // note: We are not using "ws" or "wss" for websocket. The request we are
448 // forwarding is http(s), and we don't yet know if the backend even supports
449 // websockets.
450
451 // todo: add Forwarded header? is anyone using it?
452
453 // If we see an Upgrade: websocket, we're going to assume the client needs
454 // websocket and only attempt to talk websocket with the backend. If the backend
455 // doesn't do websocket, we'll send back a "bad request" response. For other values
456 // of Upgrade, we don't do anything special.
457 // https://www.iana.org/assignments/http-upgrade-tokens/http-upgrade-tokens.xhtml
458 // Upgrade: ../rfc/9110:2798
459 // Upgrade headers are not for http/1.0, ../rfc/9110:2880
460 // Websocket client "handshake" is described at ../rfc/6455:1134
461 upgrade := r.Header.Get("Upgrade")
462 if upgrade != "" && !(r.ProtoMajor == 1 && r.ProtoMinor == 0) {
463 // Websockets have case-insensitive string "websocket".
464 for _, s := range strings.Split(upgrade, ",") {
465 if strings.EqualFold(textproto.TrimString(s), "websocket") {
466 forwardWebsocket(h, w, r, path)
467 return true
468 }
469 }
470 }
471
472 // ReverseProxy will append any remaining path to the configured target URL.
473 proxy := httputil.NewSingleHostReverseProxy(h.TargetURL)
474 proxy.FlushInterval = time.Duration(-1) // Flush after each write.
475 proxy.ErrorLog = golog.New(mlog.LogWriter(mlog.New("net/http/httputil", nil).WithContext(r.Context()), mlog.LevelDebug, "reverseproxy error"), "", 0)
476 proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
477 if errors.Is(err, context.Canceled) {
478 log().Debugx("forwarding request to backend webserver", err, slog.Any("url", r.URL))
479 return
480 }
481 log().Errorx("forwarding request to backend webserver", err, slog.Any("url", r.URL))
482 if os.IsTimeout(err) {
483 http.Error(w, "504 - gateway timeout"+recvid(r), http.StatusGatewayTimeout)
484 } else {
485 http.Error(w, "502 - bad gateway"+recvid(r), http.StatusBadGateway)
486 }
487 }
488 whdr := w.Header()
489 for k, v := range h.ResponseHeaders {
490 whdr.Add(k, v)
491 }
492 proxy.ServeHTTP(w, r)
493 return true
494}
495
496var errResponseNotWebsocket = errors.New("not a valid websocket response to request")
497var errNotImplemented = errors.New("functionality not yet implemented")
498
499// Request has an Upgrade: websocket header. Check more websocketiness about the
500// request. If it looks good, we forward it to the backend. If the backend responds
501// with a valid websocket response, indicating it is indeed a websocket server, we
502// pass the response along and start copying data between the client and the
503// backend. We don't look at the frames and payloads. The backend already needs to
504// know enough websocket to handle the frames. It wouldn't necessarily hurt to
505// monitor the frames too, and check if they are valid, but it's quite a bit of
506// work for little benefit. Besides, the whole point of websockets is to exchange
507// bytes without HTTP being in the way, so let's do that.
508func forwardWebsocket(h *config.WebForward, w http.ResponseWriter, r *http.Request, path string) (handled bool) {
509 log := func() mlog.Log {
510 return pkglog.WithContext(r.Context())
511 }
512
513 lw := w.(*loggingWriter)
514 lw.WebsocketRequest = true // For correct protocol in metrics.
515
516 // We check the requested websocket version first. A future websocket version may
517 // have different request requirements.
518 // ../rfc/6455:1160
519 wsversion := r.Header.Get("Sec-WebSocket-Version")
520 if wsversion != "13" {
521 // Indicate we only support version 13. Should get a client from the future to fall back to version 13.
522 // ../rfc/6455:1435
523 w.Header().Set("Sec-WebSocket-Version", "13")
524 http.Error(w, "400 - bad request - websockets only supported with version 13"+recvid(r), http.StatusBadRequest)
525 lw.error(fmt.Errorf("Sec-WebSocket-Version %q not supported", wsversion))
526 return true
527 }
528
529 // ../rfc/6455:1143
530 if r.Method != "GET" {
531 http.Error(w, "400 - bad request - websockets only allowed with method GET"+recvid(r), http.StatusBadRequest)
532 lw.error(fmt.Errorf("websocket request only allowed with method GET"))
533 return true
534 }
535
536 // ../rfc/6455:1153
537 var connectionUpgrade bool
538 for _, s := range strings.Split(r.Header.Get("Connection"), ",") {
539 if strings.EqualFold(textproto.TrimString(s), "upgrade") {
540 connectionUpgrade = true
541 break
542 }
543 }
544 if !connectionUpgrade {
545 http.Error(w, "400 - bad request - connection header must be \"upgrade\""+recvid(r), http.StatusBadRequest)
546 lw.error(fmt.Errorf(`connection header is %q, must be "upgrade"`, r.Header.Get("Connection")))
547 return true
548 }
549
550 // ../rfc/6455:1156
551 wskey := r.Header.Get("Sec-WebSocket-Key")
552 key, err := base64.StdEncoding.DecodeString(wskey)
553 if err != nil || len(key) != 16 {
554 http.Error(w, "400 - bad request - websockets requires Sec-WebSocket-Key with 16 bytes base64-encoded value"+recvid(r), http.StatusBadRequest)
555 lw.error(fmt.Errorf("bad Sec-WebSocket-Key %q, must be 16 byte base64-encoded value", wskey))
556 return true
557 }
558
559 // ../rfc/6455:1162
560 // We don't look at the origin header. The backend needs to handle it, if it thinks
561 // that helps...
562 // We also don't look at Sec-WebSocket-Protocol and Sec-WebSocket-Extensions. The
563 // backend can set them, but it doesn't influence our forwarding of the data.
564
565 // If this is not a hijacker, there is not point in connecting to the backend.
566 hj, ok := lw.W.(http.Hijacker)
567 var cbr *bufio.ReadWriter
568 if !ok {
569 log().Info("cannot turn http connection into tcp connection (http.Hijacker)")
570 http.Error(w, "501 - not implemented - cannot turn this connection into websocket"+recvid(r), http.StatusNotImplemented)
571 lw.error(fmt.Errorf("connection not a http.Hijacker (%T)", lw.W))
572 return
573 }
574
575 freq := *r
576 freq.Proto = "HTTP/1.1"
577 freq.ProtoMajor = 1
578 freq.ProtoMinor = 1
579 fresp, beconn, err := websocketTransact(r.Context(), h.TargetURL, &freq)
580 if err != nil {
581 if errors.Is(err, errResponseNotWebsocket) {
582 http.Error(w, "400 - bad request - websocket not supported"+recvid(r), http.StatusBadRequest)
583 } else if errors.Is(err, errNotImplemented) {
584 http.Error(w, "501 - not implemented - "+err.Error()+recvid(r), http.StatusNotImplemented)
585 } else if os.IsTimeout(err) {
586 http.Error(w, "504 - gateway timeout"+recvid(r), http.StatusGatewayTimeout)
587 } else {
588 http.Error(w, "502 - bad gateway"+recvid(r), http.StatusBadGateway)
589 }
590 lw.error(err)
591 return
592 }
593 defer func() {
594 if beconn != nil {
595 beconn.Close()
596 }
597 }()
598
599 // Hijack the client connection so we can write the response ourselves, and start
600 // copying the websocket frames.
601 var cconn net.Conn
602 cconn, cbr, err = hj.Hijack()
603 if err != nil {
604 log().Debugx("cannot turn http transaction into websocket connection", err)
605 http.Error(w, "501 - not implemented - cannot turn this connection into websocket"+recvid(r), http.StatusNotImplemented)
606 lw.error(err)
607 return
608 }
609 defer func() {
610 if cconn != nil {
611 cconn.Close()
612 }
613 }()
614
615 // Below this point, we can no longer write to the ResponseWriter.
616
617 // Mark as websocket response, for logging.
618 lw.WebsocketResponse = true
619 lw.setStatusCode(fresp.StatusCode)
620
621 for k, v := range h.ResponseHeaders {
622 fresp.Header.Add(k, v)
623 }
624
625 // Write the response to the client, completing its websocket handshake.
626 if err := fresp.Write(cconn); err != nil {
627 lw.error(fmt.Errorf("writing websocket response to client: %w", err))
628 return
629 }
630
631 errc := make(chan error, 1)
632
633 // Copy from client to backend.
634 go func() {
635 buf, err := cbr.Peek(cbr.Reader.Buffered())
636 if err != nil {
637 errc <- err
638 return
639 }
640 if len(buf) > 0 {
641 n, err := beconn.Write(buf)
642 if err != nil {
643 errc <- err
644 return
645 }
646 lw.SizeFromClient += int64(n)
647 }
648 n, err := io.Copy(beconn, cconn)
649 lw.SizeFromClient += n
650 errc <- err
651 }()
652
653 // Copy from backend to client.
654 go func() {
655 n, err := io.Copy(cconn, beconn)
656 lw.SizeToClient = n
657 errc <- err
658 }()
659
660 // Stop and close connection on first error from either size, typically a closed
661 // connection whose closing was already announced with a websocket frame.
662 lw.error(<-errc)
663 // Close connections so other goroutine stops as well.
664 cconn.Close()
665 beconn.Close()
666 // Wait for goroutine so it has updated the logWriter.Size*Client fields before we
667 // continue with logging.
668 <-errc
669 cconn = nil
670 return true
671}
672
673func websocketTransact(ctx context.Context, targetURL *url.URL, r *http.Request) (rresp *http.Response, rconn net.Conn, rerr error) {
674 log := func() mlog.Log {
675 return pkglog.WithContext(r.Context())
676 }
677
678 // Dial the backend, possibly doing TLS. We assume the net/http DefaultTransport is
679 // unmodified.
680 transport := http.DefaultTransport.(*http.Transport)
681
682 // We haven't implemented using a proxy for websocket requests yet. If we need one,
683 // return an error instead of trying to connect directly, which would be a
684 // potential security issue.
685 treq := *r
686 treq.URL = targetURL
687 if purl, err := transport.Proxy(&treq); err != nil {
688 return nil, nil, fmt.Errorf("determining proxy for websocket backend connection: %w", err)
689 } else if purl != nil {
690 return nil, nil, fmt.Errorf("%w: proxy required for websocket connection to backend", errNotImplemented) // todo: implement?
691 }
692
693 host, port, err := net.SplitHostPort(targetURL.Host)
694 if err != nil {
695 host = targetURL.Host
696 if targetURL.Scheme == "https" {
697 port = "443"
698 } else {
699 port = "80"
700 }
701 }
702 addr := net.JoinHostPort(host, port)
703 conn, err := transport.DialContext(r.Context(), "tcp", addr)
704 if err != nil {
705 return nil, nil, fmt.Errorf("dial: %w", err)
706 }
707 if targetURL.Scheme == "https" {
708 tlsconn := tls.Client(conn, transport.TLSClientConfig)
709 ctx, cancel := context.WithTimeout(r.Context(), transport.TLSHandshakeTimeout)
710 defer cancel()
711 if err := tlsconn.HandshakeContext(ctx); err != nil {
712 return nil, nil, fmt.Errorf("tls handshake: %w", err)
713 }
714 conn = tlsconn
715 }
716 defer func() {
717 if rerr != nil {
718 conn.Close()
719 }
720 }()
721
722 // todo: make timeout configurable?
723 if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
724 log().Check(err, "set deadline for websocket request to backend")
725 }
726
727 // Set clean connection headers.
728 removeHopByHopHeaders(r.Header)
729 r.Header.Set("Connection", "Upgrade")
730 r.Header.Set("Upgrade", "websocket")
731
732 // Write the websocket request to the backend.
733 if err := r.Write(conn); err != nil {
734 return nil, nil, fmt.Errorf("writing request to backend: %w", err)
735 }
736
737 // Read response from backend.
738 br := bufio.NewReader(conn)
739 resp, err := http.ReadResponse(br, r)
740 if err != nil {
741 return nil, nil, fmt.Errorf("reading response from backend: %w", err)
742 }
743 defer func() {
744 if rerr != nil {
745 resp.Body.Close()
746 }
747 }()
748 if err := conn.SetDeadline(time.Time{}); err != nil {
749 log().Check(err, "clearing deadline on websocket connection to backend")
750 }
751
752 // Check that the response from the backend server indicates it is websocket. If
753 // not, don't pass the backend response, but an error that websocket is not
754 // appropriate.
755 if err := checkWebsocketResponse(resp, r); err != nil {
756 return resp, nil, err
757 }
758
759 // note: net/http.Response.Body documents that it implements io.Writer for a
760 // status: 101 response. But that's not the case when the response has been read
761 // with http.ReadResponse. We'll write to the connection directly.
762
763 buf, err := br.Peek(br.Buffered())
764 if err != nil {
765 return resp, nil, fmt.Errorf("peek at buffered data written by backend: %w", err)
766 }
767 return resp, websocketConn{io.MultiReader(bytes.NewReader(buf), conn), conn}, nil
768}
769
770// A net.Conn but with reads coming from an io multireader (due to buffered reader
771// needed for http.ReadResponse).
772type websocketConn struct {
773 r io.Reader
774 net.Conn
775}
776
777func (c websocketConn) Read(buf []byte) (int, error) {
778 return c.r.Read(buf)
779}
780
781// Check that an HTTP response (from a backend) is a valid websocket response, i.e.
782// that it accepts the WebSocket "upgrade".
783// ../rfc/6455:1299
784func checkWebsocketResponse(resp *http.Response, req *http.Request) error {
785 if resp.StatusCode != 101 {
786 return fmt.Errorf("%w: response http status not 101 but %s", errResponseNotWebsocket, resp.Status)
787 }
788 if upgrade := resp.Header.Get("Upgrade"); !strings.EqualFold(upgrade, "websocket") {
789 return fmt.Errorf(`%w: response http status is 101, but Upgrade header is %q, should be "websocket"`, errResponseNotWebsocket, upgrade)
790 }
791 if connection := resp.Header.Get("Connection"); !strings.EqualFold(connection, "upgrade") {
792 return fmt.Errorf(`%w: response http status is 101, Upgrade is websocket, but Connection header is %q, should be "Upgrade"`, errResponseNotWebsocket, connection)
793 }
794 accept, err := base64.StdEncoding.DecodeString(resp.Header.Get("Sec-WebSocket-Accept"))
795 if err != nil {
796 return fmt.Errorf(`%w: response http status, Upgrade and Connection header are websocket, but Sec-WebSocket-Accept header is not valid base64: %v`, errResponseNotWebsocket, err)
797 }
798 exp := sha1.Sum([]byte(req.Header.Get("Sec-WebSocket-Key") + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
799 if !bytes.Equal(accept, exp[:]) {
800 return fmt.Errorf(`%w: response http status, Upgrade and Connection header are websocket, but backend Sec-WebSocket-Accept value does not match`, errResponseNotWebsocket)
801 }
802 // We don't have requirements for the other Sec-WebSocket headers. ../rfc/6455:1340
803 return nil
804}
805
806// From Go 1.20.4 src/net/http/httputil/reverseproxy.go:
807// Hop-by-hop headers. These are removed when sent to the backend.
808// As of RFC 7230, hop-by-hop headers are required to appear in the
809// Connection header field. These are the headers defined by the
810// obsoleted RFC 2616 (section 13.5.1) and are used for backward
811// compatibility.
812// ../rfc/2616:5128
813var hopHeaders = []string{
814 "Connection",
815 "Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
816 "Keep-Alive",
817 "Proxy-Authenticate",
818 "Proxy-Authorization",
819 "Te", // canonicalized version of "TE"
820 "Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
821 "Transfer-Encoding",
822 "Upgrade",
823}
824
825// From Go 1.20.4 src/net/http/httputil/reverseproxy.go:
826// removeHopByHopHeaders removes hop-by-hop headers.
827func removeHopByHopHeaders(h http.Header) {
828 // RFC 7230, section 6.1: Remove headers listed in the "Connection" header.
829 // ../rfc/7230:2817
830 for _, f := range h["Connection"] {
831 for _, sf := range strings.Split(f, ",") {
832 if sf = textproto.TrimString(sf); sf != "" {
833 h.Del(sf)
834 }
835 }
836 }
837 // RFC 2616, section 13.5.1: Remove a set of known hop-by-hop headers.
838 // This behavior is superseded by the RFC 7230 Connection header, but
839 // preserve it for backwards compatibility.
840 // ../rfc/2616:5128
841 for _, f := range hopHeaders {
842 h.Del(f)
843 }
844}
845