1// Package http provides HTTP listeners/servers, for
2// autoconfiguration/autodiscovery, the account and admin web interface and
25 "golang.org/x/exp/maps"
27 "github.com/prometheus/client_golang/prometheus"
28 "github.com/prometheus/client_golang/prometheus/promauto"
29 "github.com/prometheus/client_golang/prometheus/promhttp"
31 "github.com/mjl-/mox/autotls"
32 "github.com/mjl-/mox/config"
33 "github.com/mjl-/mox/dns"
34 "github.com/mjl-/mox/mlog"
35 "github.com/mjl-/mox/mox-"
36 "github.com/mjl-/mox/ratelimit"
37 "github.com/mjl-/mox/webaccount"
38 "github.com/mjl-/mox/webadmin"
39 "github.com/mjl-/mox/webapisrv"
40 "github.com/mjl-/mox/webmail"
43var pkglog = mlog.New("http", nil)
46 // metricRequest tracks performance (time to write response header) of server.
47 metricRequest = promauto.NewHistogramVec(
48 prometheus.HistogramOpts{
49 Name: "mox_httpserver_request_duration_seconds",
50 Help: "HTTP(s) server request with handler name, protocol, method, result codes, and duration until response status code is written, in seconds.",
51 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
54 "handler", // Name from webhandler, can be empty.
55 "proto", // "http", "https", "ws", "wss"
56 "method", // "(unknown)" and otherwise only common verbs
60 // metricResponse tracks performance of entire request as experienced by users,
61 // which also depends on their connection speed, so not necessarily something you
63 metricResponse = promauto.NewHistogramVec(
64 prometheus.HistogramOpts{
65 Name: "mox_httpserver_response_duration_seconds",
66 Help: "HTTP(s) server response with handler name, protocol, method, result codes, and duration of entire response, in seconds.",
67 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
70 "handler", // Name from webhandler, can be empty.
71 "proto", // "http", "https", "ws", "wss"
72 "method", // "(unknown)" and otherwise only common verbs
78// We serve a favicon when webaccount/webmail/webadmin/webapi for account-related
79// domains. They are configured as "service handler", which have a lower priority
80// than web handler. Admins can configure a custom /favicon.ico route to override
81// the builtin favicon. In the future, we may want to make it easier to customize
82// the favicon, possibly per client settings domain.
86var faviconModTime = time.Now()
89 p, err := os.Executable()
91 if st, err := os.Stat(p); err == nil {
92 faviconModTime = st.ModTime()
97func faviconHandle(w http.ResponseWriter, r *http.Request) {
98 http.ServeContent(w, r, "favicon.ico", faviconModTime, strings.NewReader(faviconIco))
101type responseWriterFlusher interface {
106// http.ResponseWriter that writes access log and tracks metrics at end of response.
107type loggingWriter struct {
108 W responseWriterFlusher // Calls are forwarded.
111 WebsocketRequest bool // Whether request from was websocket.
119 Size int64 // Of data served to client, for non-websocket responses.
120 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
121 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
123 WebsocketResponse bool // If this was a successful websocket connection with backend.
124 SizeFromClient, SizeToClient int64 // Websocket data.
125 Attrs []slog.Attr // Additional fields to log.
128func (w *loggingWriter) AddAttr(a slog.Attr) {
129 w.Attrs = append(w.Attrs, a)
132func (w *loggingWriter) Flush() {
136func (w *loggingWriter) Header() http.Header {
140// protocol, for logging.
141func (w *loggingWriter) proto(websocket bool) string {
152func (w *loggingWriter) Write(buf []byte) (int, error) {
153 if w.StatusCode == 0 {
154 w.WriteHeader(http.StatusOK)
160 n, err = w.W.Write(buf)
165 // We flush after each write. Probably takes a few more bytes, but prevents any
166 // issues due to buffering.
167 // w.Gzip.Write updates w.Size with the compressed byte count.
168 n, err = w.Gzip.Write(buf)
173 w.UncompressedSize += int64(n)
182func (w *loggingWriter) setStatusCode(statusCode int) {
183 if w.StatusCode != 0 {
187 w.StatusCode = statusCode
188 method := metricHTTPMethod(w.R.Method)
189 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
192// SetUncompressedSize is used through an interface by
193// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
194func (w *loggingWriter) SetUncompressedSize(origSize int64) {
195 w.UncompressedSize = origSize
198func (w *loggingWriter) WriteHeader(statusCode int) {
199 if w.StatusCode != 0 {
203 w.setStatusCode(statusCode)
205 // We transparently gzip-compress responses for requests under these conditions, all must apply:
207 // - Enabled for handler (static handlers make their own decisions).
208 // - Not a websocket request.
209 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
210 // - Not already compressed, or any other Content-Encoding header (including "identity").
211 // - Client accepts gzip encoded responses.
212 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
213 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")) {
214 // 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.
216 // We track the gzipped output for the access log.
217 cw := countWriter{Writer: w.W, Size: &w.Size}
218 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
219 w.W.Header().Set("Content-Encoding", "gzip")
220 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
222 w.W.WriteHeader(statusCode)
225func acceptsGzip(r *http.Request) bool {
226 s := r.Header.Get("Accept-Encoding")
227 t := strings.Split(s, ",")
228 for _, e := range t {
229 e = strings.TrimSpace(e)
230 tt := strings.Split(e, ";")
231 if len(tt) > 1 && t[1] == "q=0" {
241var compressibleTypes = map[string]bool{
242 "application/csv": true,
243 "application/javascript": true,
244 "application/json": true,
245 "application/x-javascript": true,
246 "application/xml": true,
247 "image/vnd.microsoft.icon": true,
248 "image/x-icon": true,
252 "font/opentype": true,
255func compressibleContentType(ct string) bool {
256 ct = strings.SplitN(ct, ";", 2)[0]
257 ct = strings.TrimSpace(ct)
258 ct = strings.ToLower(ct)
259 if compressibleTypes[ct] {
262 t, st, _ := strings.Cut(ct, "/")
263 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
266func compressibleContent(f *os.File) bool {
267 // We don't want to store many small files. They take up too much disk overhead.
268 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
272 buf := make([]byte, 512)
273 n, err := f.ReadAt(buf, 0)
274 if err != nil && err != io.EOF {
277 ct := http.DetectContentType(buf[:n])
278 return compressibleContentType(ct)
281type countWriter struct {
286func (w countWriter) Write(buf []byte) (int, error) {
287 n, err := w.Writer.Write(buf)
294var tlsVersions = map[uint16]string{
295 tls.VersionTLS10: "tls1.0",
296 tls.VersionTLS11: "tls1.1",
297 tls.VersionTLS12: "tls1.2",
298 tls.VersionTLS13: "tls1.3",
301func metricHTTPMethod(method string) string {
302 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
303 method = strings.ToLower(method)
305 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":
311func (w *loggingWriter) error(err error) {
317func (w *loggingWriter) Done() {
318 if w.Err == nil && w.Gzip != nil {
319 if err := w.Gzip.Close(); err != nil {
324 method := metricHTTPMethod(w.R.Method)
325 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
329 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
337 err = w.R.Context().Err()
339 attrs := []slog.Attr{
340 slog.String("httpaccess", ""),
341 slog.String("handler", w.Handler),
342 slog.String("method", method),
343 slog.Any("url", w.R.URL),
344 slog.String("host", w.R.Host),
345 slog.Duration("duration", time.Since(w.Start)),
346 slog.Int("statuscode", w.StatusCode),
347 slog.String("proto", strings.ToLower(w.R.Proto)),
348 slog.Any("remoteaddr", w.R.RemoteAddr),
349 slog.String("tlsinfo", tlsinfo),
350 slog.String("useragent", w.R.Header.Get("User-Agent")),
351 slog.String("referrr", w.R.Header.Get("Referrer")),
353 if w.WebsocketRequest {
354 attrs = append(attrs,
355 slog.Bool("websocketrequest", true),
358 if w.WebsocketResponse {
359 attrs = append(attrs,
360 slog.Bool("websocket", true),
361 slog.Int64("sizetoclient", w.SizeToClient),
362 slog.Int64("sizefromclient", w.SizeFromClient),
364 } else if w.UncompressedSize > 0 {
365 attrs = append(attrs,
366 slog.Int64("size", w.Size),
367 slog.Int64("uncompressedsize", w.UncompressedSize),
370 attrs = append(attrs,
371 slog.Int64("size", w.Size),
374 attrs = append(attrs, w.Attrs...)
375 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
378// Built-in handlers, e.g. mta-sts and autoconfig.
379type pathHandler struct {
380 Name string // For logging/metrics.
381 HostMatch func(host dns.IPDomain) bool // If not nil, called to see if domain of requests matches. Host can be zero value for invalid domain/ip.
382 Path string // Path to register, like on http.ServeMux.
386 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https).
387 TLSConfig *tls.Config
390 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
391 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
392 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
393 // interfaces) last. WebHandlers can also pass requests to the internal servers.
394 // This order allows admins to serve other content on domains serving the mox.conf
395 // internal services.
396 SystemHandlers []pathHandler // Sorted, longest first.
398 ServiceHandlers []pathHandler // Sorted, longest first.
401// SystemHandle registers a named system handler for a path and optional host. If
402// path ends with a slash, it is used as prefix match, otherwise a full path match
403// is required. If hostOpt is set, only requests to those host are handled by this
405func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
406 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
409// Like SystemHandle, but for internal services "admin", "account", "webmail",
410// "webapi" configured in the mox.conf Listener.
411func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
412 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
416 limiterConnectionrate = &ratelimit.Limiter{
417 WindowLimits: []ratelimit.WindowLimit{
420 Limits: [...]int64{1000, 3000, 9000},
424 Limits: [...]int64{5000, 15000, 45000},
430// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
431// right pathHandler or WebHandler, and it generates access logs and tracks
433func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
435 // Rate limiting as early as possible.
436 ipstr, _, err := net.SplitHostPort(r.RemoteAddr)
438 pkglog.Debugx("split host:port client remoteaddr", err, slog.Any("remoteaddr", r.RemoteAddr))
439 } else if ip := net.ParseIP(ipstr); ip == nil {
440 pkglog.Debug("parsing ip for client remoteaddr", slog.Any("remoteaddr", r.RemoteAddr))
441 } else if !limiterConnectionrate.Add(ip, now, 1) {
442 method := metricHTTPMethod(r.Method)
447 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
448 // No logging, that's just noise.
450 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
454 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
455 r = r.WithContext(ctx)
457 wf, ok := xw.(responseWriterFlusher)
459 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
463 nw := &loggingWriter{
470 // Cleanup path, removing ".." and ".". Keep any trailing slash.
471 trailingPath := strings.HasSuffix(r.URL.Path, "/")
472 if r.URL.Path == "" {
475 r.URL.Path = path.Clean(r.URL.Path)
476 if r.URL.Path == "." {
479 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
484 nhost, _, err := net.SplitHostPort(host)
488 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
490 dom, domErr := dns.ParseDomain(host)
492 ipdom = dns.IPDomain{Domain: dom}
496 handle := func(h pathHandler) bool {
497 if h.HostMatch != nil && !h.HostMatch(ipdom) {
500 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
503 h.Handler.ServeHTTP(nw, r)
509 for _, h := range s.SystemHandlers {
515 if WebHandle(nw, r, ipdom) {
519 for _, h := range s.ServiceHandlers {
524 nw.Handler = "(nomatch)"
528func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
529 // Helpfully redirect user to version with ending slash.
530 if path != "/" && strings.HasSuffix(path, "/") {
531 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
532 http.Redirect(w, r, path, http.StatusSeeOther)
534 srv.ServiceHandle(name, hostMatch, path[:len(path)-1], handler)
538// Listen binds to sockets for HTTP listeners, including those required for ACME to
539// generate TLS certificates. It stores the listeners so Serve can start serving them.
541 // Initialize listeners in deterministic order for the same potential error
543 names := maps.Keys(mox.Conf.Static.Listeners)
545 for _, name := range names {
546 l := mox.Conf.Static.Listeners[name]
547 portServe := portServes(l)
549 ports := maps.Keys(portServe)
551 for _, port := range ports {
552 srv := portServe[port]
553 for _, ip := range l.IPs {
554 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv)
560func portServes(l config.Listener) map[int]*serve {
561 portServe := map[int]*serve{}
563 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
564 localhost := dns.Domain{ASCII: "localhost"}
566 ldom := l.HostnameDomain
567 if l.Hostname == "" {
568 ldom = mox.Conf.Static.HostnameDomain
570 listenerHostMatch := func(host dns.IPDomain) bool {
574 return host.Domain == ldom || host.Domain == localhost
576 accountHostMatch := func(host dns.IPDomain) bool {
577 if listenerHostMatch(host) {
580 return mox.Conf.IsClientSettingsDomain(host.Domain)
583 var ensureServe func(https bool, port int, kind string, favicon bool) *serve
584 ensureServe = func(https bool, port int, kind string, favicon bool) *serve {
587 s = &serve{nil, nil, false, nil, false, nil}
590 s.Kinds = append(s.Kinds, kind)
591 if favicon && !s.Favicon {
592 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
596 if https && l.TLS.ACME != "" {
597 s.TLSConfig = l.TLS.ACMEConfig
599 s.TLSConfig = l.TLS.Config
600 if l.TLS.ACME != "" {
601 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
602 ensureServe(true, tlsport, "acme-tls-alpn-01", false)
608 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
609 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
610 ensureServe(true, port, "acme-tls-alpn-01", false)
613 if l.AccountHTTP.Enabled {
614 port := config.Port(l.AccountHTTP.Port, 80)
616 if l.AccountHTTP.Path != "" {
617 path = l.AccountHTTP.Path
619 srv := ensureServe(false, port, "account-http at "+path, true)
620 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
621 srv.ServiceHandle("account", accountHostMatch, path, handler)
622 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
624 if l.AccountHTTPS.Enabled {
625 port := config.Port(l.AccountHTTPS.Port, 443)
627 if l.AccountHTTPS.Path != "" {
628 path = l.AccountHTTPS.Path
630 srv := ensureServe(true, port, "account-https at "+path, true)
631 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
632 srv.ServiceHandle("account", accountHostMatch, path, handler)
633 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
636 if l.AdminHTTP.Enabled {
637 port := config.Port(l.AdminHTTP.Port, 80)
639 if l.AdminHTTP.Path != "" {
640 path = l.AdminHTTP.Path
642 srv := ensureServe(false, port, "admin-http at "+path, true)
643 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
644 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
645 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
647 if l.AdminHTTPS.Enabled {
648 port := config.Port(l.AdminHTTPS.Port, 443)
650 if l.AdminHTTPS.Path != "" {
651 path = l.AdminHTTPS.Path
653 srv := ensureServe(true, port, "admin-https at "+path, true)
654 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
655 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
656 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
659 maxMsgSize := l.SMTPMaxMessageSize
661 maxMsgSize = config.DefaultMaxMsgSize
664 if l.WebAPIHTTP.Enabled {
665 port := config.Port(l.WebAPIHTTP.Port, 80)
667 if l.WebAPIHTTP.Path != "" {
668 path = l.WebAPIHTTP.Path
670 srv := ensureServe(false, port, "webapi-http at "+path, true)
671 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
672 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
673 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
675 if l.WebAPIHTTPS.Enabled {
676 port := config.Port(l.WebAPIHTTPS.Port, 443)
678 if l.WebAPIHTTPS.Path != "" {
679 path = l.WebAPIHTTPS.Path
681 srv := ensureServe(true, port, "webapi-https at "+path, true)
682 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
683 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
684 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
687 if l.WebmailHTTP.Enabled {
688 port := config.Port(l.WebmailHTTP.Port, 80)
690 if l.WebmailHTTP.Path != "" {
691 path = l.WebmailHTTP.Path
693 srv := ensureServe(false, port, "webmail-http at "+path, true)
694 var accountPath string
695 if l.AccountHTTP.Enabled {
697 if l.AccountHTTP.Path != "" {
698 accountPath = l.AccountHTTP.Path
701 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
702 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
703 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
705 if l.WebmailHTTPS.Enabled {
706 port := config.Port(l.WebmailHTTPS.Port, 443)
708 if l.WebmailHTTPS.Path != "" {
709 path = l.WebmailHTTPS.Path
711 srv := ensureServe(true, port, "webmail-https at "+path, true)
712 var accountPath string
713 if l.AccountHTTPS.Enabled {
715 if l.AccountHTTPS.Path != "" {
716 accountPath = l.AccountHTTPS.Path
719 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
720 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
721 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
724 if l.MetricsHTTP.Enabled {
725 port := config.Port(l.MetricsHTTP.Port, 8010)
726 srv := ensureServe(false, port, "metrics-http", false)
727 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
728 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
729 if r.URL.Path != "/" {
732 } else if r.Method != "GET" {
733 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
736 w.Header().Set("Content-Type", "text/html")
737 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
740 if l.AutoconfigHTTPS.Enabled {
741 port := config.Port(l.AutoconfigHTTPS.Port, 443)
742 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https", false)
743 autoconfigMatch := func(ipdom dns.IPDomain) bool {
748 // Thunderbird requests an autodiscovery URL at the email address domain name, so
749 // autoconfig prefix is optional.
750 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
751 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
752 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
754 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
755 // use the mail server's host name.
756 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
759 dc, ok := mox.Conf.Domain(dom)
760 return ok && !dc.ReportsOnly
762 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
763 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
764 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
765 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
767 if l.MTASTSHTTPS.Enabled {
768 port := config.Port(l.MTASTSHTTPS.Port, 443)
769 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https", false)
770 mtastsMatch := func(ipdom dns.IPDomain) bool {
771 // todo: may want to check this against the configured domains, could in theory be just a webserver.
776 return strings.HasPrefix(dom.ASCII, "mta-sts.")
778 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
780 if l.PprofHTTP.Enabled {
781 // Importing net/http/pprof registers handlers on the default serve mux.
782 port := config.Port(l.PprofHTTP.Port, 8011)
783 if _, ok := portServe[port]; ok {
784 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
786 srv := &serve{[]string{"pprof-http"}, nil, false, nil, false, nil}
787 portServe[port] = srv
788 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
790 if l.WebserverHTTP.Enabled {
791 port := config.Port(l.WebserverHTTP.Port, 80)
792 srv := ensureServe(false, port, "webserver-http", false)
795 if l.WebserverHTTPS.Enabled {
796 port := config.Port(l.WebserverHTTPS.Port, 443)
797 srv := ensureServe(true, port, "webserver-https", false)
801 if l.TLS != nil && l.TLS.ACME != "" {
802 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
804 // If we are listening on port 80 for plain http, also register acme http-01
805 // validation handler.
806 if srv, ok := portServe[80]; ok && srv.TLSConfig == nil {
807 srv.Kinds = append(srv.Kinds, "acme-http-01")
808 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
811 hosts := map[dns.Domain]struct{}{
812 mox.Conf.Static.HostnameDomain: {},
814 if l.HostnameDomain.ASCII != "" {
815 hosts[l.HostnameDomain] = struct{}{}
817 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
818 // presence of TLS certificates for.
819 for _, name := range mox.Conf.Domains() {
820 if dom, err := dns.ParseDomain(name); err != nil {
821 pkglog.Errorx("parsing domain from config", err)
822 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
823 // Do not gather autoconfig name if we aren't accepting email for this domain.
827 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
829 pkglog.Errorx("parsing domain from config for autoconfig", err)
831 hosts[autoconfdom] = struct{}{}
835 ensureManagerHosts[m] = hosts
838 for _, srv := range portServe {
839 sortPathHandlers(srv.SystemHandlers)
840 sortPathHandlers(srv.ServiceHandlers)
846func sortPathHandlers(l []pathHandler) {
847 sort.Slice(l, func(i, j int) bool {
850 if len(a) == len(b) {
851 // For consistent order.
854 // Longest paths first.
855 return len(a) > len(b)
859// functions to be launched in goroutine that will serve on a listener.
862// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
863// immediately after startup. We only do so for our explicit listener hostnames,
864// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
865// do request autoconfig, otherwise clients may run into their timeouts waiting for
866// the certificate to be given during the first https connection.
867var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
869// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
870func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler) {
871 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
876 if tlsConfig == nil {
878 if os.Getuid() == 0 {
879 pkglog.Print("http listener",
880 slog.String("name", name),
881 slog.String("kinds", strings.Join(kinds, ",")),
882 slog.String("address", addr))
884 ln, err = mox.Listen(mox.Network(ip), addr)
886 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
890 if os.Getuid() == 0 {
891 pkglog.Print("https listener",
892 slog.String("name", name),
893 slog.String("kinds", strings.Join(kinds, ",")),
894 slog.String("address", addr))
896 ln, err = mox.Listen(mox.Network(ip), addr)
898 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
900 ln = tls.NewListener(ln, tlsConfig)
903 server := &http.Server{
905 // Clone because our multiple Server.Serve calls modify config concurrently leading to data race.
906 TLSConfig: tlsConfig.Clone(),
907 ReadHeaderTimeout: 30 * time.Second,
908 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
909 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
912 err := server.Serve(ln)
913 pkglog.Fatalx(protocol+": serve", err)
915 servers = append(servers, serve)
918// Serve starts serving on the initialized listeners.
920 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
922 go webaccount.ImportManage()
924 for _, serve := range servers {
930 time.Sleep(1 * time.Second)
932 for m, hosts := range ensureManagerHosts {
933 for host := range hosts {
934 // Check if certificate is already available. If so, we don't print as much after a
935 // restart, and finish more quickly if only a few certificates are missing/old.
936 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
937 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
943 // Just in case someone adds quite some domains to their config. We don't want to
944 // hit any ACME rate limits.
948 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
949 time.Sleep(10 * time.Second)
953 hello := &tls.ClientHelloInfo{
954 ServerName: host.ASCII,
956 // Make us fetch an ECDSA P256 cert.
957 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
958 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
959 SupportedCurves: []tls.CurveID{tls.CurveP256},
960 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
961 SupportedVersions: []uint16{tls.VersionTLS13},
963 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
964 if _, err := m.Manager.GetCertificate(hello); err != nil {
965 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))