1// Package http provides HTTP listeners/servers, for
2// autoconfiguration/autodiscovery, the account and admin web interface and
24 "golang.org/x/exp/maps"
26 "github.com/prometheus/client_golang/prometheus"
27 "github.com/prometheus/client_golang/prometheus/promauto"
28 "github.com/prometheus/client_golang/prometheus/promhttp"
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"
42var pkglog = mlog.New("http", nil)
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},
53 "handler", // Name from webhandler, can be empty.
54 "proto", // "http", "https", "ws", "wss"
55 "method", // "(unknown)" and otherwise only common verbs
59 // metricResponse tracks performance of entire request as experienced by users,
60 // which also depends on their connection speed, so not necessarily something you
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},
69 "handler", // Name from webhandler, can be empty.
70 "proto", // "http", "https", "ws", "wss"
71 "method", // "(unknown)" and otherwise only common verbs
77type responseWriterFlusher interface {
82// http.ResponseWriter that writes access log and tracks metrics at end of response.
83type loggingWriter struct {
84 W responseWriterFlusher // Calls are forwarded.
87 WebsocketRequest bool // Whether request from was websocket.
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).
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.
104func (w *loggingWriter) AddAttr(a slog.Attr) {
105 w.Attrs = append(w.Attrs, a)
108func (w *loggingWriter) Flush() {
112func (w *loggingWriter) Header() http.Header {
116// protocol, for logging.
117func (w *loggingWriter) proto(websocket bool) string {
128func (w *loggingWriter) Write(buf []byte) (int, error) {
129 if w.StatusCode == 0 {
130 w.WriteHeader(http.StatusOK)
136 n, err = w.W.Write(buf)
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)
149 w.UncompressedSize += int64(n)
158func (w *loggingWriter) setStatusCode(statusCode int) {
159 if w.StatusCode != 0 {
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))
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
174func (w *loggingWriter) WriteHeader(statusCode int) {
175 if w.StatusCode != 0 {
179 w.setStatusCode(statusCode)
181 // We transparently gzip-compress responses for requests under these conditions, all must apply:
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.
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.
198 w.W.WriteHeader(statusCode)
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" {
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,
228 "font/opentype": true,
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] {
238 t, st, _ := strings.Cut(ct, "/")
239 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
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 {
248 buf := make([]byte, 512)
249 n, err := f.ReadAt(buf, 0)
250 if err != nil && err != io.EOF {
253 ct := http.DetectContentType(buf[:n])
254 return compressibleContentType(ct)
257type countWriter struct {
262func (w countWriter) Write(buf []byte) (int, error) {
263 n, err := w.Writer.Write(buf)
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",
277func metricHTTPMethod(method string) string {
278 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
279 method = strings.ToLower(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":
287func (w *loggingWriter) error(err error) {
293func (w *loggingWriter) Done() {
294 if w.Err == nil && w.Gzip != nil {
295 if err := w.Gzip.Close(); err != nil {
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))
305 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
313 err = w.R.Context().Err()
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")),
329 if w.WebsocketRequest {
330 attrs = append(attrs,
331 slog.Bool("websocketrequest", true),
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),
340 } else if w.UncompressedSize > 0 {
341 attrs = append(attrs,
342 slog.Int64("size", w.Size),
343 slog.Int64("uncompressedsize", w.UncompressedSize),
346 attrs = append(attrs,
347 slog.Int64("size", w.Size),
350 attrs = append(attrs, w.Attrs...)
351 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
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) {
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")
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.
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.
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})
388 limiterConnectionrate = &ratelimit.Limiter{
389 WindowLimits: []ratelimit.WindowLimit{
392 Limits: [...]int64{1000, 3000, 9000},
396 Limits: [...]int64{5000, 15000, 45000},
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
405func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
407 // Rate limiting as early as possible.
408 ipstr, _, err := net.SplitHostPort(r.RemoteAddr)
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)
419 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
420 // No logging, that's just noise.
422 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
426 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
427 r = r.WithContext(ctx)
429 wf, ok := xw.(responseWriterFlusher)
431 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
435 nw := &loggingWriter{
442 // Cleanup path, removing ".." and ".". Keep any trailing slash.
443 trailingPath := strings.HasSuffix(r.URL.Path, "/")
444 if r.URL.Path == "" {
447 r.URL.Path = path.Clean(r.URL.Path)
448 if r.URL.Path == "." {
451 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
457 nhost, _, err := net.SplitHostPort(host)
461 // host could be an IP, some handles may match, not an error.
462 dom, domErr := dns.ParseDomain(host)
464 for _, h := range s.PathHandlers {
465 if h.HostMatch != nil && (domErr != nil || !h.HostMatch(dom)) {
468 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
471 h.Handler.ServeHTTP(nw, r)
475 if s.Webserver && domErr == nil {
476 if WebHandle(nw, r, dom) {
480 nw.Handler = "(nomatch)"
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.
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)
493 srv.Handle(name, nil, path[:len(path)-1], handler)
497 // Initialize listeners in deterministic order for the same potential error
499 names := maps.Keys(mox.Conf.Static.Listeners)
501 for _, name := range names {
502 l := mox.Conf.Static.Listeners[name]
504 portServe := map[int]*serve{}
506 var ensureServe func(https bool, port int, kind string) *serve
507 ensureServe = func(https bool, port int, kind string) *serve {
510 s = &serve{nil, nil, nil, false}
513 s.Kinds = append(s.Kinds, kind)
514 if https && l.TLS.ACME != "" {
515 s.TLSConfig = l.TLS.ACMEConfig
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")
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")
531 if l.AccountHTTP.Enabled {
532 port := config.Port(l.AccountHTTP.Port, 80)
534 if l.AccountHTTP.Path != "" {
535 path = l.AccountHTTP.Path
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)
542 if l.AccountHTTPS.Enabled {
543 port := config.Port(l.AccountHTTPS.Port, 443)
545 if l.AccountHTTPS.Path != "" {
546 path = l.AccountHTTPS.Path
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)
554 if l.AdminHTTP.Enabled {
555 port := config.Port(l.AdminHTTP.Port, 80)
557 if l.AdminHTTP.Path != "" {
558 path = l.AdminHTTP.Path
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)
565 if l.AdminHTTPS.Enabled {
566 port := config.Port(l.AdminHTTPS.Port, 443)
568 if l.AdminHTTPS.Path != "" {
569 path = l.AdminHTTPS.Path
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)
577 maxMsgSize := l.SMTPMaxMessageSize
579 maxMsgSize = config.DefaultMaxMsgSize
582 if l.WebAPIHTTP.Enabled {
583 port := config.Port(l.WebAPIHTTP.Port, 80)
585 if l.WebAPIHTTP.Path != "" {
586 path = l.WebAPIHTTP.Path
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)
593 if l.WebAPIHTTPS.Enabled {
594 port := config.Port(l.WebAPIHTTPS.Port, 443)
596 if l.WebAPIHTTPS.Path != "" {
597 path = l.WebAPIHTTPS.Path
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)
605 if l.WebmailHTTP.Enabled {
606 port := config.Port(l.WebmailHTTP.Port, 80)
608 if l.WebmailHTTP.Path != "" {
609 path = l.WebmailHTTP.Path
611 srv := ensureServe(false, port, "webmail-http at "+path)
612 var accountPath string
613 if l.AccountHTTP.Enabled {
615 if l.AccountHTTP.Path != "" {
616 accountPath = l.AccountHTTP.Path
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)
623 if l.WebmailHTTPS.Enabled {
624 port := config.Port(l.WebmailHTTPS.Port, 443)
626 if l.WebmailHTTPS.Path != "" {
627 path = l.WebmailHTTPS.Path
629 srv := ensureServe(true, port, "webmail-https at "+path)
630 var accountPath string
631 if l.AccountHTTPS.Enabled {
633 if l.AccountHTTPS.Path != "" {
634 accountPath = l.AccountHTTPS.Path
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)
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 != "/" {
650 } else if r.Method != "GET" {
651 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
654 w.Header().Set("Content-Type", "text/html")
655 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
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.")
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 {
673 dc, ok := mox.Conf.Domain(dom)
674 return ok && !dc.ReportsOnly
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)))
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.")
688 srv.Handle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", safeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
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")
696 srv := &serve{[]string{"pprof-http"}, nil, nil, false}
697 portServe[port] = srv
698 srv.Handle("pprof", nil, "/", http.DefaultServeMux)
700 if l.WebserverHTTP.Enabled {
701 port := config.Port(l.WebserverHTTP.Port, 80)
702 srv := ensureServe(false, port, "webserver-http")
705 if l.WebserverHTTPS.Enabled {
706 port := config.Port(l.WebserverHTTPS.Port, 443)
707 srv := ensureServe(true, port, "webserver-https")
711 if l.TLS != nil && l.TLS.ACME != "" {
712 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
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))
721 hosts := map[dns.Domain]struct{}{
722 mox.Conf.Static.HostnameDomain: {},
724 if l.HostnameDomain.ASCII != "" {
725 hosts[l.HostnameDomain] = struct{}{}
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.
737 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
739 pkglog.Errorx("parsing domain from config for autoconfig", err)
741 hosts[autoconfdom] = struct{}{}
745 ensureManagerHosts[m] = hosts
748 ports := maps.Keys(portServe)
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.
759 // Longest paths first.
760 return len(a) > len(b)
762 for _, ip := range l.IPs {
763 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv)
769// functions to be launched in goroutine that will serve on a listener.
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{}{}
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))
786 if tlsConfig == nil {
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))
794 ln, err = mox.Listen(mox.Network(ip), addr)
796 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
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))
806 ln, err = mox.Listen(mox.Network(ip), addr)
808 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
810 ln = tls.NewListener(ln, tlsConfig)
813 server := &http.Server{
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),
822 err := server.Serve(ln)
823 pkglog.Fatalx(protocol+": serve", err)
825 servers = append(servers, serve)
828// Serve starts serving on the initialized listeners.
830 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
832 go webaccount.ImportManage()
834 for _, serve := range servers {
840 time.Sleep(1 * time.Second)
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))
853 // Just in case someone adds quite some domains to their config. We don't want to
854 // hit any ACME rate limits.
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)
863 hello := &tls.ClientHelloInfo{
864 ServerName: host.ASCII,
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},
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))