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