1// Package http provides HTTP listeners/servers, for
2// autoconfiguration/autodiscovery, the account and admin web interface and
3// MTA-STS policies.
4package http
5
6import (
7 "compress/gzip"
8 "context"
9 "crypto/tls"
10 "fmt"
11 "io"
12 golog "log"
13 "log/slog"
14 "net"
15 "net/http"
16 "os"
17 "path"
18 "sort"
19 "strings"
20 "time"
21
22 _ "net/http/pprof"
23
24 "golang.org/x/exp/maps"
25
26 "github.com/prometheus/client_golang/prometheus"
27 "github.com/prometheus/client_golang/prometheus/promauto"
28 "github.com/prometheus/client_golang/prometheus/promhttp"
29
30 "github.com/mjl-/mox/autotls"
31 "github.com/mjl-/mox/config"
32 "github.com/mjl-/mox/dns"
33 "github.com/mjl-/mox/mlog"
34 "github.com/mjl-/mox/mox-"
35 "github.com/mjl-/mox/ratelimit"
36 "github.com/mjl-/mox/webaccount"
37 "github.com/mjl-/mox/webadmin"
38 "github.com/mjl-/mox/webapisrv"
39 "github.com/mjl-/mox/webmail"
40)
41
42var pkglog = mlog.New("http", nil)
43
44var (
45 // metricRequest tracks performance (time to write response header) of server.
46 metricRequest = promauto.NewHistogramVec(
47 prometheus.HistogramOpts{
48 Name: "mox_httpserver_request_duration_seconds",
49 Help: "HTTP(s) server request with handler name, protocol, method, result codes, and duration until response status code is written, in seconds.",
50 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
51 },
52 []string{
53 "handler", // Name from webhandler, can be empty.
54 "proto", // "http", "https", "ws", "wss"
55 "method", // "(unknown)" and otherwise only common verbs
56 "code",
57 },
58 )
59 // metricResponse tracks performance of entire request as experienced by users,
60 // which also depends on their connection speed, so not necessarily something you
61 // could act on.
62 metricResponse = promauto.NewHistogramVec(
63 prometheus.HistogramOpts{
64 Name: "mox_httpserver_response_duration_seconds",
65 Help: "HTTP(s) server response with handler name, protocol, method, result codes, and duration of entire response, in seconds.",
66 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
67 },
68 []string{
69 "handler", // Name from webhandler, can be empty.
70 "proto", // "http", "https", "ws", "wss"
71 "method", // "(unknown)" and otherwise only common verbs
72 "code",
73 },
74 )
75)
76
77type responseWriterFlusher interface {
78 http.ResponseWriter
79 http.Flusher
80}
81
82// http.ResponseWriter that writes access log and tracks metrics at end of response.
83type loggingWriter struct {
84 W responseWriterFlusher // Calls are forwarded.
85 Start time.Time
86 R *http.Request
87 WebsocketRequest bool // Whether request from was websocket.
88
89 // Set by router.
90 Handler string
91 Compress bool
92
93 // Set by handlers.
94 StatusCode int
95 Size int64 // Of data served to client, for non-websocket responses.
96 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
97 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
98 Err error
99 WebsocketResponse bool // If this was a successful websocket connection with backend.
100 SizeFromClient, SizeToClient int64 // Websocket data.
101 Attrs []slog.Attr // Additional fields to log.
102}
103
104func (w *loggingWriter) AddAttr(a slog.Attr) {
105 w.Attrs = append(w.Attrs, a)
106}
107
108func (w *loggingWriter) Flush() {
109 w.W.Flush()
110}
111
112func (w *loggingWriter) Header() http.Header {
113 return w.W.Header()
114}
115
116// protocol, for logging.
117func (w *loggingWriter) proto(websocket bool) string {
118 proto := "http"
119 if websocket {
120 proto = "ws"
121 }
122 if w.R.TLS != nil {
123 proto += "s"
124 }
125 return proto
126}
127
128func (w *loggingWriter) Write(buf []byte) (int, error) {
129 if w.StatusCode == 0 {
130 w.WriteHeader(http.StatusOK)
131 }
132
133 var n int
134 var err error
135 if w.Gzip == nil {
136 n, err = w.W.Write(buf)
137 if n > 0 {
138 w.Size += int64(n)
139 }
140 } else {
141 // We flush after each write. Probably takes a few more bytes, but prevents any
142 // issues due to buffering.
143 // w.Gzip.Write updates w.Size with the compressed byte count.
144 n, err = w.Gzip.Write(buf)
145 if err == nil {
146 err = w.Gzip.Flush()
147 }
148 if n > 0 {
149 w.UncompressedSize += int64(n)
150 }
151 }
152 if err != nil {
153 w.error(err)
154 }
155 return n, err
156}
157
158func (w *loggingWriter) setStatusCode(statusCode int) {
159 if w.StatusCode != 0 {
160 return
161 }
162
163 w.StatusCode = statusCode
164 method := metricHTTPMethod(w.R.Method)
165 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
166}
167
168// SetUncompressedSize is used through an interface by
169// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
170func (w *loggingWriter) SetUncompressedSize(origSize int64) {
171 w.UncompressedSize = origSize
172}
173
174func (w *loggingWriter) WriteHeader(statusCode int) {
175 if w.StatusCode != 0 {
176 return
177 }
178
179 w.setStatusCode(statusCode)
180
181 // We transparently gzip-compress responses for requests under these conditions, all must apply:
182 //
183 // - Enabled for handler (static handlers make their own decisions).
184 // - Not a websocket request.
185 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
186 // - Not already compressed, or any other Content-Encoding header (including "identity").
187 // - Client accepts gzip encoded responses.
188 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
189 if w.Compress && !w.WebsocketRequest && statusCode == http.StatusOK && w.W.Header().Values("Content-Encoding") == nil && acceptsGzip(w.R) && compressibleContentType(w.W.Header().Get("Content-Type")) {
190 // todo: we should gather the first kb of data, see if it is compressible. if not, just return original. should set timer so we flush if it takes too long to gather 1kb. for smaller data we shouldn't compress at all.
191
192 // We track the gzipped output for the access log.
193 cw := countWriter{Writer: w.W, Size: &w.Size}
194 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
195 w.W.Header().Set("Content-Encoding", "gzip")
196 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
197 }
198 w.W.WriteHeader(statusCode)
199}
200
201func acceptsGzip(r *http.Request) bool {
202 s := r.Header.Get("Accept-Encoding")
203 t := strings.Split(s, ",")
204 for _, e := range t {
205 e = strings.TrimSpace(e)
206 tt := strings.Split(e, ";")
207 if len(tt) > 1 && t[1] == "q=0" {
208 continue
209 }
210 if tt[0] == "gzip" {
211 return true
212 }
213 }
214 return false
215}
216
217var compressibleTypes = map[string]bool{
218 "application/csv": true,
219 "application/javascript": true,
220 "application/json": true,
221 "application/x-javascript": true,
222 "application/xml": true,
223 "image/vnd.microsoft.icon": true,
224 "image/x-icon": true,
225 "font/ttf": true,
226 "font/eot": true,
227 "font/otf": true,
228 "font/opentype": true,
229}
230
231func compressibleContentType(ct string) bool {
232 ct = strings.SplitN(ct, ";", 2)[0]
233 ct = strings.TrimSpace(ct)
234 ct = strings.ToLower(ct)
235 if compressibleTypes[ct] {
236 return true
237 }
238 t, st, _ := strings.Cut(ct, "/")
239 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
240}
241
242func compressibleContent(f *os.File) bool {
243 // We don't want to store many small files. They take up too much disk overhead.
244 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
245 return false
246 }
247
248 buf := make([]byte, 512)
249 n, err := f.ReadAt(buf, 0)
250 if err != nil && err != io.EOF {
251 return false
252 }
253 ct := http.DetectContentType(buf[:n])
254 return compressibleContentType(ct)
255}
256
257type countWriter struct {
258 Writer io.Writer
259 Size *int64
260}
261
262func (w countWriter) Write(buf []byte) (int, error) {
263 n, err := w.Writer.Write(buf)
264 if n > 0 {
265 *w.Size += int64(n)
266 }
267 return n, err
268}
269
270var tlsVersions = map[uint16]string{
271 tls.VersionTLS10: "tls1.0",
272 tls.VersionTLS11: "tls1.1",
273 tls.VersionTLS12: "tls1.2",
274 tls.VersionTLS13: "tls1.3",
275}
276
277func metricHTTPMethod(method string) string {
278 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
279 method = strings.ToLower(method)
280 switch method {
281 case "acl", "baseline-control", "bind", "checkin", "checkout", "connect", "copy", "delete", "get", "head", "label", "link", "lock", "merge", "mkactivity", "mkcalendar", "mkcol", "mkredirectref", "mkworkspace", "move", "options", "orderpatch", "patch", "post", "pri", "propfind", "proppatch", "put", "rebind", "report", "search", "trace", "unbind", "uncheckout", "unlink", "unlock", "update", "updateredirectref", "version-control":
282 return method
283 }
284 return "(other)"
285}
286
287func (w *loggingWriter) error(err error) {
288 if w.Err == nil {
289 w.Err = err
290 }
291}
292
293func (w *loggingWriter) Done() {
294 if w.Err == nil && w.Gzip != nil {
295 if err := w.Gzip.Close(); err != nil {
296 w.error(err)
297 }
298 }
299
300 method := metricHTTPMethod(w.R.Method)
301 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
302
303 tlsinfo := "plain"
304 if w.R.TLS != nil {
305 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
306 tlsinfo = v
307 } else {
308 tlsinfo = "(other)"
309 }
310 }
311 err := w.Err
312 if err == nil {
313 err = w.R.Context().Err()
314 }
315 attrs := []slog.Attr{
316 slog.String("httpaccess", ""),
317 slog.String("handler", w.Handler),
318 slog.String("method", method),
319 slog.Any("url", w.R.URL),
320 slog.String("host", w.R.Host),
321 slog.Duration("duration", time.Since(w.Start)),
322 slog.Int("statuscode", w.StatusCode),
323 slog.String("proto", strings.ToLower(w.R.Proto)),
324 slog.Any("remoteaddr", w.R.RemoteAddr),
325 slog.String("tlsinfo", tlsinfo),
326 slog.String("useragent", w.R.Header.Get("User-Agent")),
327 slog.String("referrr", w.R.Header.Get("Referrer")),
328 }
329 if w.WebsocketRequest {
330 attrs = append(attrs,
331 slog.Bool("websocketrequest", true),
332 )
333 }
334 if w.WebsocketResponse {
335 attrs = append(attrs,
336 slog.Bool("websocket", true),
337 slog.Int64("sizetoclient", w.SizeToClient),
338 slog.Int64("sizefromclient", w.SizeFromClient),
339 )
340 } else if w.UncompressedSize > 0 {
341 attrs = append(attrs,
342 slog.Int64("size", w.Size),
343 slog.Int64("uncompressedsize", w.UncompressedSize),
344 )
345 } else {
346 attrs = append(attrs,
347 slog.Int64("size", w.Size),
348 )
349 }
350 attrs = append(attrs, w.Attrs...)
351 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
352}
353
354// Set some http headers that should prevent potential abuse. Better safe than sorry.
355func safeHeaders(fn http.Handler) http.Handler {
356 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
357 h := w.Header()
358 h.Set("X-Frame-Options", "deny")
359 h.Set("X-Content-Type-Options", "nosniff")
360 h.Set("Content-Security-Policy", "default-src 'self' 'unsafe-inline' data:")
361 h.Set("Referrer-Policy", "same-origin")
362 fn.ServeHTTP(w, r)
363 })
364}
365
366// Built-in handlers, e.g. mta-sts and autoconfig.
367type pathHandler struct {
368 Name string // For logging/metrics.
369 HostMatch func(dom dns.Domain) bool // If not nil, called to see if domain of requests matches. Only called if requested host is a valid domain.
370 Path string // Path to register, like on http.ServeMux.
371 Handler http.Handler
372}
373type serve struct {
374 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https).
375 TLSConfig *tls.Config
376 PathHandlers []pathHandler // Sorted, longest first.
377 Webserver bool // Whether serving WebHandler. PathHandlers are always evaluated before WebHandlers.
378}
379
380// Handle registers a named handler for a path and optional host. If path ends with
381// a slash, it is used as prefix match, otherwise a full path match is required. If
382// hostOpt is set, only requests to those host are handled by this handler.
383func (s *serve) Handle(name string, hostMatch func(dns.Domain) bool, path string, fn http.Handler) {
384 s.PathHandlers = append(s.PathHandlers, pathHandler{name, hostMatch, path, fn})
385}
386
387var (
388 limiterConnectionrate = &ratelimit.Limiter{
389 WindowLimits: []ratelimit.WindowLimit{
390 {
391 Window: time.Minute,
392 Limits: [...]int64{1000, 3000, 9000},
393 },
394 {
395 Window: time.Hour,
396 Limits: [...]int64{5000, 15000, 45000},
397 },
398 },
399 }
400)
401
402// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
403// right pathHandler or WebHandler, and it generates access logs and tracks
404// metrics.
405func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
406 now := time.Now()
407 // Rate limiting as early as possible.
408 ipstr, _, err := net.SplitHostPort(r.RemoteAddr)
409 if err != nil {
410 pkglog.Debugx("split host:port client remoteaddr", err, slog.Any("remoteaddr", r.RemoteAddr))
411 } else if ip := net.ParseIP(ipstr); ip == nil {
412 pkglog.Debug("parsing ip for client remoteaddr", slog.Any("remoteaddr", r.RemoteAddr))
413 } else if !limiterConnectionrate.Add(ip, now, 1) {
414 method := metricHTTPMethod(r.Method)
415 proto := "http"
416 if r.TLS != nil {
417 proto = "https"
418 }
419 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
420 // No logging, that's just noise.
421
422 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
423 return
424 }
425
426 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
427 r = r.WithContext(ctx)
428
429 wf, ok := xw.(responseWriterFlusher)
430 if !ok {
431 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
432 return
433 }
434
435 nw := &loggingWriter{
436 W: wf,
437 Start: now,
438 R: r,
439 }
440 defer nw.Done()
441
442 // Cleanup path, removing ".." and ".". Keep any trailing slash.
443 trailingPath := strings.HasSuffix(r.URL.Path, "/")
444 if r.URL.Path == "" {
445 r.URL.Path = "/"
446 }
447 r.URL.Path = path.Clean(r.URL.Path)
448 if r.URL.Path == "." {
449 r.URL.Path = "/"
450 }
451 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
452 r.URL.Path += "/"
453 }
454
455 var dom dns.Domain
456 host := r.Host
457 nhost, _, err := net.SplitHostPort(host)
458 if err == nil {
459 host = nhost
460 }
461 // host could be an IP, some handles may match, not an error.
462 dom, domErr := dns.ParseDomain(host)
463
464 for _, h := range s.PathHandlers {
465 if h.HostMatch != nil && (domErr != nil || !h.HostMatch(dom)) {
466 continue
467 }
468 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
469 nw.Handler = h.Name
470 nw.Compress = true
471 h.Handler.ServeHTTP(nw, r)
472 return
473 }
474 }
475 if s.Webserver && domErr == nil {
476 if WebHandle(nw, r, dom) {
477 return
478 }
479 }
480 nw.Handler = "(nomatch)"
481 http.NotFound(nw, r)
482}
483
484// Listen binds to sockets for HTTP listeners, including those required for ACME to
485// generate TLS certificates. It stores the listeners so Serve can start serving them.
486func Listen() {
487 redirectToTrailingSlash := func(srv *serve, name, path string) {
488 // Helpfully redirect user to version with ending slash.
489 if path != "/" && strings.HasSuffix(path, "/") {
490 handler := safeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
491 http.Redirect(w, r, path, http.StatusSeeOther)
492 }))
493 srv.Handle(name, nil, path[:len(path)-1], handler)
494 }
495 }
496
497 // Initialize listeners in deterministic order for the same potential error
498 // messages.
499 names := maps.Keys(mox.Conf.Static.Listeners)
500 sort.Strings(names)
501 for _, name := range names {
502 l := mox.Conf.Static.Listeners[name]
503
504 portServe := map[int]*serve{}
505
506 var ensureServe func(https bool, port int, kind string) *serve
507 ensureServe = func(https bool, port int, kind string) *serve {
508 s := portServe[port]
509 if s == nil {
510 s = &serve{nil, nil, nil, false}
511 portServe[port] = s
512 }
513 s.Kinds = append(s.Kinds, kind)
514 if https && l.TLS.ACME != "" {
515 s.TLSConfig = l.TLS.ACMEConfig
516 } else if https {
517 s.TLSConfig = l.TLS.Config
518 if l.TLS.ACME != "" {
519 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
520 ensureServe(true, tlsport, "acme-tls-alpn-01")
521 }
522 }
523 return s
524 }
525
526 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
527 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
528 ensureServe(true, port, "acme-tls-alpn-01")
529 }
530
531 if l.AccountHTTP.Enabled {
532 port := config.Port(l.AccountHTTP.Port, 80)
533 path := "/"
534 if l.AccountHTTP.Path != "" {
535 path = l.AccountHTTP.Path
536 }
537 srv := ensureServe(false, port, "account-http at "+path)
538 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
539 srv.Handle("account", nil, path, handler)
540 redirectToTrailingSlash(srv, "account", path)
541 }
542 if l.AccountHTTPS.Enabled {
543 port := config.Port(l.AccountHTTPS.Port, 443)
544 path := "/"
545 if l.AccountHTTPS.Path != "" {
546 path = l.AccountHTTPS.Path
547 }
548 srv := ensureServe(true, port, "account-https at "+path)
549 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
550 srv.Handle("account", nil, path, handler)
551 redirectToTrailingSlash(srv, "account", path)
552 }
553
554 if l.AdminHTTP.Enabled {
555 port := config.Port(l.AdminHTTP.Port, 80)
556 path := "/admin/"
557 if l.AdminHTTP.Path != "" {
558 path = l.AdminHTTP.Path
559 }
560 srv := ensureServe(false, port, "admin-http at "+path)
561 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
562 srv.Handle("admin", nil, path, handler)
563 redirectToTrailingSlash(srv, "admin", path)
564 }
565 if l.AdminHTTPS.Enabled {
566 port := config.Port(l.AdminHTTPS.Port, 443)
567 path := "/admin/"
568 if l.AdminHTTPS.Path != "" {
569 path = l.AdminHTTPS.Path
570 }
571 srv := ensureServe(true, port, "admin-https at "+path)
572 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
573 srv.Handle("admin", nil, path, handler)
574 redirectToTrailingSlash(srv, "admin", path)
575 }
576
577 maxMsgSize := l.SMTPMaxMessageSize
578 if maxMsgSize == 0 {
579 maxMsgSize = config.DefaultMaxMsgSize
580 }
581
582 if l.WebAPIHTTP.Enabled {
583 port := config.Port(l.WebAPIHTTP.Port, 80)
584 path := "/webapi/"
585 if l.WebAPIHTTP.Path != "" {
586 path = l.WebAPIHTTP.Path
587 }
588 srv := ensureServe(false, port, "webapi-http at "+path)
589 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
590 srv.Handle("webapi", nil, path, handler)
591 redirectToTrailingSlash(srv, "webapi", path)
592 }
593 if l.WebAPIHTTPS.Enabled {
594 port := config.Port(l.WebAPIHTTPS.Port, 443)
595 path := "/webapi/"
596 if l.WebAPIHTTPS.Path != "" {
597 path = l.WebAPIHTTPS.Path
598 }
599 srv := ensureServe(true, port, "webapi-https at "+path)
600 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
601 srv.Handle("webapi", nil, path, handler)
602 redirectToTrailingSlash(srv, "webapi", path)
603 }
604
605 if l.WebmailHTTP.Enabled {
606 port := config.Port(l.WebmailHTTP.Port, 80)
607 path := "/webmail/"
608 if l.WebmailHTTP.Path != "" {
609 path = l.WebmailHTTP.Path
610 }
611 srv := ensureServe(false, port, "webmail-http at "+path)
612 var accountPath string
613 if l.AccountHTTP.Enabled {
614 accountPath = "/"
615 if l.AccountHTTP.Path != "" {
616 accountPath = l.AccountHTTP.Path
617 }
618 }
619 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
620 srv.Handle("webmail", nil, path, handler)
621 redirectToTrailingSlash(srv, "webmail", path)
622 }
623 if l.WebmailHTTPS.Enabled {
624 port := config.Port(l.WebmailHTTPS.Port, 443)
625 path := "/webmail/"
626 if l.WebmailHTTPS.Path != "" {
627 path = l.WebmailHTTPS.Path
628 }
629 srv := ensureServe(true, port, "webmail-https at "+path)
630 var accountPath string
631 if l.AccountHTTPS.Enabled {
632 accountPath = "/"
633 if l.AccountHTTPS.Path != "" {
634 accountPath = l.AccountHTTPS.Path
635 }
636 }
637 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
638 srv.Handle("webmail", nil, path, handler)
639 redirectToTrailingSlash(srv, "webmail", path)
640 }
641
642 if l.MetricsHTTP.Enabled {
643 port := config.Port(l.MetricsHTTP.Port, 8010)
644 srv := ensureServe(false, port, "metrics-http")
645 srv.Handle("metrics", nil, "/metrics", safeHeaders(promhttp.Handler()))
646 srv.Handle("metrics", nil, "/", safeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
647 if r.URL.Path != "/" {
648 http.NotFound(w, r)
649 return
650 } else if r.Method != "GET" {
651 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
652 return
653 }
654 w.Header().Set("Content-Type", "text/html")
655 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
656 })))
657 }
658 if l.AutoconfigHTTPS.Enabled {
659 port := config.Port(l.AutoconfigHTTPS.Port, 443)
660 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https")
661 autoconfigMatch := func(dom dns.Domain) bool {
662 // Thunderbird requests an autodiscovery URL at the email address domain name, so
663 // autoconfig prefix is optional.
664 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
665 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
666 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
667 }
668 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
669 // use the mail server's host name.
670 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
671 return true
672 }
673 dc, ok := mox.Conf.Domain(dom)
674 return ok && !dc.ReportsOnly
675 }
676 srv.Handle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", safeHeaders(http.HandlerFunc(autoconfHandle)))
677 srv.Handle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", safeHeaders(http.HandlerFunc(autodiscoverHandle)))
678 srv.Handle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", safeHeaders(http.HandlerFunc(mobileconfigHandle)))
679 srv.Handle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", safeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
680 }
681 if l.MTASTSHTTPS.Enabled {
682 port := config.Port(l.MTASTSHTTPS.Port, 443)
683 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https")
684 mtastsMatch := func(dom dns.Domain) bool {
685 // todo: may want to check this against the configured domains, could in theory be just a webserver.
686 return strings.HasPrefix(dom.ASCII, "mta-sts.")
687 }
688 srv.Handle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", safeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
689 }
690 if l.PprofHTTP.Enabled {
691 // Importing net/http/pprof registers handlers on the default serve mux.
692 port := config.Port(l.PprofHTTP.Port, 8011)
693 if _, ok := portServe[port]; ok {
694 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
695 }
696 srv := &serve{[]string{"pprof-http"}, nil, nil, false}
697 portServe[port] = srv
698 srv.Handle("pprof", nil, "/", http.DefaultServeMux)
699 }
700 if l.WebserverHTTP.Enabled {
701 port := config.Port(l.WebserverHTTP.Port, 80)
702 srv := ensureServe(false, port, "webserver-http")
703 srv.Webserver = true
704 }
705 if l.WebserverHTTPS.Enabled {
706 port := config.Port(l.WebserverHTTPS.Port, 443)
707 srv := ensureServe(true, port, "webserver-https")
708 srv.Webserver = true
709 }
710
711 if l.TLS != nil && l.TLS.ACME != "" {
712 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
713
714 // If we are listening on port 80 for plain http, also register acme http-01
715 // validation handler.
716 if srv, ok := portServe[80]; ok && srv.TLSConfig == nil {
717 srv.Kinds = append(srv.Kinds, "acme-http-01")
718 srv.Handle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
719 }
720
721 hosts := map[dns.Domain]struct{}{
722 mox.Conf.Static.HostnameDomain: {},
723 }
724 if l.HostnameDomain.ASCII != "" {
725 hosts[l.HostnameDomain] = struct{}{}
726 }
727 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
728 // presence of TLS certificates for.
729 for _, name := range mox.Conf.Domains() {
730 if dom, err := dns.ParseDomain(name); err != nil {
731 pkglog.Errorx("parsing domain from config", err)
732 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
733 // Do not gather autoconfig name if we aren't accepting email for this domain.
734 continue
735 }
736
737 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
738 if err != nil {
739 pkglog.Errorx("parsing domain from config for autoconfig", err)
740 } else {
741 hosts[autoconfdom] = struct{}{}
742 }
743 }
744
745 ensureManagerHosts[m] = hosts
746 }
747
748 ports := maps.Keys(portServe)
749 sort.Ints(ports)
750 for _, port := range ports {
751 srv := portServe[port]
752 sort.Slice(srv.PathHandlers, func(i, j int) bool {
753 a := srv.PathHandlers[i].Path
754 b := srv.PathHandlers[j].Path
755 if len(a) == len(b) {
756 // For consistent order.
757 return a < b
758 }
759 // Longest paths first.
760 return len(a) > len(b)
761 })
762 for _, ip := range l.IPs {
763 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv)
764 }
765 }
766 }
767}
768
769// functions to be launched in goroutine that will serve on a listener.
770var servers []func()
771
772// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
773// immediately after startup. We only do so for our explicit listener hostnames,
774// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
775// do request autoconfig, otherwise clients may run into their timeouts waiting for
776// the certificate to be given during the first https connection.
777var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
778
779// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
780func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler) {
781 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
782
783 var protocol string
784 var ln net.Listener
785 var err error
786 if tlsConfig == nil {
787 protocol = "http"
788 if os.Getuid() == 0 {
789 pkglog.Print("http listener",
790 slog.String("name", name),
791 slog.String("kinds", strings.Join(kinds, ",")),
792 slog.String("address", addr))
793 }
794 ln, err = mox.Listen(mox.Network(ip), addr)
795 if err != nil {
796 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
797 }
798 } else {
799 protocol = "https"
800 if os.Getuid() == 0 {
801 pkglog.Print("https listener",
802 slog.String("name", name),
803 slog.String("kinds", strings.Join(kinds, ",")),
804 slog.String("address", addr))
805 }
806 ln, err = mox.Listen(mox.Network(ip), addr)
807 if err != nil {
808 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
809 }
810 ln = tls.NewListener(ln, tlsConfig)
811 }
812
813 server := &http.Server{
814 Handler: handler,
815 // Clone because our multiple Server.Serve calls modify config concurrently leading to data race.
816 TLSConfig: tlsConfig.Clone(),
817 ReadHeaderTimeout: 30 * time.Second,
818 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
819 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
820 }
821 serve := func() {
822 err := server.Serve(ln)
823 pkglog.Fatalx(protocol+": serve", err)
824 }
825 servers = append(servers, serve)
826}
827
828// Serve starts serving on the initialized listeners.
829func Serve() {
830 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
831
832 go webaccount.ImportManage()
833
834 for _, serve := range servers {
835 go serve()
836 }
837 servers = nil
838
839 go func() {
840 time.Sleep(1 * time.Second)
841 i := 0
842 for m, hosts := range ensureManagerHosts {
843 for host := range hosts {
844 // Check if certificate is already available. If so, we don't print as much after a
845 // restart, and finish more quickly if only a few certificates are missing/old.
846 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
847 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
848 } else if avail {
849 continue
850 }
851
852 if i >= 10 {
853 // Just in case someone adds quite some domains to their config. We don't want to
854 // hit any ACME rate limits.
855 return
856 }
857 if i > 0 {
858 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
859 time.Sleep(10 * time.Second)
860 }
861 i++
862
863 hello := &tls.ClientHelloInfo{
864 ServerName: host.ASCII,
865
866 // Make us fetch an ECDSA P256 cert.
867 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
868 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
869 SupportedCurves: []tls.CurveID{tls.CurveP256},
870 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
871 SupportedVersions: []uint16{tls.VersionTLS13},
872 }
873 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
874 if _, err := m.Manager.GetCertificate(hello); err != nil {
875 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))
876 }
877 }
878 }
879 }()
880}
881