1// Package http provides HTTP listeners/servers, for
2// autoconfiguration/autodiscovery, the account and admin web interface and
26 "golang.org/x/exp/maps"
28 "github.com/prometheus/client_golang/prometheus"
29 "github.com/prometheus/client_golang/prometheus/promauto"
30 "github.com/prometheus/client_golang/prometheus/promhttp"
32 "github.com/mjl-/mox/autotls"
33 "github.com/mjl-/mox/config"
34 "github.com/mjl-/mox/dns"
35 "github.com/mjl-/mox/mlog"
36 "github.com/mjl-/mox/mox-"
37 "github.com/mjl-/mox/ratelimit"
38 "github.com/mjl-/mox/webaccount"
39 "github.com/mjl-/mox/webadmin"
40 "github.com/mjl-/mox/webapisrv"
41 "github.com/mjl-/mox/webmail"
44var pkglog = mlog.New("http", nil)
47 // metricRequest tracks performance (time to write response header) of server.
48 metricRequest = promauto.NewHistogramVec(
49 prometheus.HistogramOpts{
50 Name: "mox_httpserver_request_duration_seconds",
51 Help: "HTTP(s) server request with handler name, protocol, method, result codes, and duration until response status code is written, in seconds.",
52 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
55 "handler", // Name from webhandler, can be empty.
56 "proto", // "http", "https", "ws", "wss"
57 "method", // "(unknown)" and otherwise only common verbs
61 // metricResponse tracks performance of entire request as experienced by users,
62 // which also depends on their connection speed, so not necessarily something you
64 metricResponse = promauto.NewHistogramVec(
65 prometheus.HistogramOpts{
66 Name: "mox_httpserver_response_duration_seconds",
67 Help: "HTTP(s) server response with handler name, protocol, method, result codes, and duration of entire response, in seconds.",
68 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
71 "handler", // Name from webhandler, can be empty.
72 "proto", // "http", "https", "ws", "wss"
73 "method", // "(unknown)" and otherwise only common verbs
79// We serve a favicon when webaccount/webmail/webadmin/webapi for account-related
80// domains. They are configured as "service handler", which have a lower priority
81// than web handler. Admins can configure a custom /favicon.ico route to override
82// the builtin favicon. In the future, we may want to make it easier to customize
83// the favicon, possibly per client settings domain.
87var faviconModTime = time.Now()
90 p, err := os.Executable()
92 if st, err := os.Stat(p); err == nil {
93 faviconModTime = st.ModTime()
98func faviconHandle(w http.ResponseWriter, r *http.Request) {
99 http.ServeContent(w, r, "favicon.ico", faviconModTime, strings.NewReader(faviconIco))
102type responseWriterFlusher interface {
107// http.ResponseWriter that writes access log and tracks metrics at end of response.
108type loggingWriter struct {
109 W responseWriterFlusher // Calls are forwarded.
112 WebsocketRequest bool // Whether request from was websocket.
120 Size int64 // Of data served to client, for non-websocket responses.
121 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
122 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
124 WebsocketResponse bool // If this was a successful websocket connection with backend.
125 SizeFromClient, SizeToClient int64 // Websocket data.
126 Attrs []slog.Attr // Additional fields to log.
129func (w *loggingWriter) AddAttr(a slog.Attr) {
130 w.Attrs = append(w.Attrs, a)
133func (w *loggingWriter) Flush() {
137func (w *loggingWriter) Header() http.Header {
141// protocol, for logging.
142func (w *loggingWriter) proto(websocket bool) string {
153func (w *loggingWriter) Write(buf []byte) (int, error) {
154 if w.StatusCode == 0 {
155 w.WriteHeader(http.StatusOK)
161 n, err = w.W.Write(buf)
166 // We flush after each write. Probably takes a few more bytes, but prevents any
167 // issues due to buffering.
168 // w.Gzip.Write updates w.Size with the compressed byte count.
169 n, err = w.Gzip.Write(buf)
174 w.UncompressedSize += int64(n)
183func (w *loggingWriter) setStatusCode(statusCode int) {
184 if w.StatusCode != 0 {
188 w.StatusCode = statusCode
189 method := metricHTTPMethod(w.R.Method)
190 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
193// SetUncompressedSize is used through an interface by
194// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
195func (w *loggingWriter) SetUncompressedSize(origSize int64) {
196 w.UncompressedSize = origSize
199func (w *loggingWriter) WriteHeader(statusCode int) {
200 if w.StatusCode != 0 {
204 w.setStatusCode(statusCode)
206 // We transparently gzip-compress responses for requests under these conditions, all must apply:
208 // - Enabled for handler (static handlers make their own decisions).
209 // - Not a websocket request.
210 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
211 // - Not already compressed, or any other Content-Encoding header (including "identity").
212 // - Client accepts gzip encoded responses.
213 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
214 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")) {
215 // 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.
217 // We track the gzipped output for the access log.
218 cw := countWriter{Writer: w.W, Size: &w.Size}
219 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
220 w.W.Header().Set("Content-Encoding", "gzip")
221 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
223 w.W.WriteHeader(statusCode)
226func acceptsGzip(r *http.Request) bool {
227 s := r.Header.Get("Accept-Encoding")
228 t := strings.Split(s, ",")
229 for _, e := range t {
230 e = strings.TrimSpace(e)
231 tt := strings.Split(e, ";")
232 if len(tt) > 1 && t[1] == "q=0" {
242var compressibleTypes = map[string]bool{
243 "application/csv": true,
244 "application/javascript": true,
245 "application/json": true,
246 "application/x-javascript": true,
247 "application/xml": true,
248 "image/vnd.microsoft.icon": true,
249 "image/x-icon": true,
253 "font/opentype": true,
256func compressibleContentType(ct string) bool {
257 ct = strings.SplitN(ct, ";", 2)[0]
258 ct = strings.TrimSpace(ct)
259 ct = strings.ToLower(ct)
260 if compressibleTypes[ct] {
263 t, st, _ := strings.Cut(ct, "/")
264 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
267func compressibleContent(f *os.File) bool {
268 // We don't want to store many small files. They take up too much disk overhead.
269 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
273 buf := make([]byte, 512)
274 n, err := f.ReadAt(buf, 0)
275 if err != nil && err != io.EOF {
278 ct := http.DetectContentType(buf[:n])
279 return compressibleContentType(ct)
282type countWriter struct {
287func (w countWriter) Write(buf []byte) (int, error) {
288 n, err := w.Writer.Write(buf)
295var tlsVersions = map[uint16]string{
296 tls.VersionTLS10: "tls1.0",
297 tls.VersionTLS11: "tls1.1",
298 tls.VersionTLS12: "tls1.2",
299 tls.VersionTLS13: "tls1.3",
302func metricHTTPMethod(method string) string {
303 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
304 method = strings.ToLower(method)
306 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":
312func (w *loggingWriter) error(err error) {
318func (w *loggingWriter) Done() {
319 if w.Err == nil && w.Gzip != nil {
320 if err := w.Gzip.Close(); err != nil {
325 method := metricHTTPMethod(w.R.Method)
326 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
330 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
338 err = w.R.Context().Err()
340 attrs := []slog.Attr{
341 slog.String("httpaccess", ""),
342 slog.String("handler", w.Handler),
343 slog.String("method", method),
344 slog.Any("url", w.R.URL),
345 slog.String("host", w.R.Host),
346 slog.Duration("duration", time.Since(w.Start)),
347 slog.Int("statuscode", w.StatusCode),
348 slog.String("proto", strings.ToLower(w.R.Proto)),
349 slog.Any("remoteaddr", w.R.RemoteAddr),
350 slog.String("tlsinfo", tlsinfo),
351 slog.String("useragent", w.R.Header.Get("User-Agent")),
352 slog.String("referrr", w.R.Header.Get("Referrer")),
354 if w.WebsocketRequest {
355 attrs = append(attrs,
356 slog.Bool("websocketrequest", true),
359 if w.WebsocketResponse {
360 attrs = append(attrs,
361 slog.Bool("websocket", true),
362 slog.Int64("sizetoclient", w.SizeToClient),
363 slog.Int64("sizefromclient", w.SizeFromClient),
365 } else if w.UncompressedSize > 0 {
366 attrs = append(attrs,
367 slog.Int64("size", w.Size),
368 slog.Int64("uncompressedsize", w.UncompressedSize),
371 attrs = append(attrs,
372 slog.Int64("size", w.Size),
375 attrs = append(attrs, w.Attrs...)
376 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
379// Built-in handlers, e.g. mta-sts and autoconfig.
380type pathHandler struct {
381 Name string // For logging/metrics.
382 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.
383 Path string // Path to register, like on http.ServeMux.
387 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https).
388 TLSConfig *tls.Config
391 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
392 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
393 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
394 // interfaces) last. WebHandlers can also pass requests to the internal servers.
395 // This order allows admins to serve other content on domains serving the mox.conf
396 // internal services.
397 SystemHandlers []pathHandler // Sorted, longest first.
399 ServiceHandlers []pathHandler // Sorted, longest first.
402// SystemHandle registers a named system handler for a path and optional host. If
403// path ends with a slash, it is used as prefix match, otherwise a full path match
404// is required. If hostOpt is set, only requests to those host are handled by this
406func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
407 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
410// Like SystemHandle, but for internal services "admin", "account", "webmail",
411// "webapi" configured in the mox.conf Listener.
412func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
413 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
417 limiterConnectionrate = &ratelimit.Limiter{
418 WindowLimits: []ratelimit.WindowLimit{
421 Limits: [...]int64{1000, 3000, 9000},
425 Limits: [...]int64{5000, 15000, 45000},
431// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
432// right pathHandler or WebHandler, and it generates access logs and tracks
434func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
436 // Rate limiting as early as possible.
437 ipstr, _, err := net.SplitHostPort(r.RemoteAddr)
439 pkglog.Debugx("split host:port client remoteaddr", err, slog.Any("remoteaddr", r.RemoteAddr))
440 } else if ip := net.ParseIP(ipstr); ip == nil {
441 pkglog.Debug("parsing ip for client remoteaddr", slog.Any("remoteaddr", r.RemoteAddr))
442 } else if !limiterConnectionrate.Add(ip, now, 1) {
443 method := metricHTTPMethod(r.Method)
448 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
449 // No logging, that's just noise.
451 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
455 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
456 r = r.WithContext(ctx)
458 wf, ok := xw.(responseWriterFlusher)
460 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
464 nw := &loggingWriter{
471 // Cleanup path, removing ".." and ".". Keep any trailing slash.
472 trailingPath := strings.HasSuffix(r.URL.Path, "/")
473 if r.URL.Path == "" {
476 r.URL.Path = path.Clean(r.URL.Path)
477 if r.URL.Path == "." {
480 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
485 nhost, _, err := net.SplitHostPort(host)
489 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
491 dom, domErr := dns.ParseDomain(host)
493 ipdom = dns.IPDomain{Domain: dom}
497 handle := func(h pathHandler) bool {
498 if h.HostMatch != nil && !h.HostMatch(ipdom) {
501 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
504 h.Handler.ServeHTTP(nw, r)
510 for _, h := range s.SystemHandlers {
516 if WebHandle(nw, r, ipdom) {
520 for _, h := range s.ServiceHandlers {
525 nw.Handler = "(nomatch)"
529func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
530 // Helpfully redirect user to version with ending slash.
531 if path != "/" && strings.HasSuffix(path, "/") {
532 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
533 http.Redirect(w, r, path, http.StatusSeeOther)
535 srv.ServiceHandle(name, hostMatch, path[:len(path)-1], handler)
539// Listen binds to sockets for HTTP listeners, including those required for ACME to
540// generate TLS certificates. It stores the listeners so Serve can start serving them.
542 // Initialize listeners in deterministic order for the same potential error
544 names := maps.Keys(mox.Conf.Static.Listeners)
546 for _, name := range names {
547 l := mox.Conf.Static.Listeners[name]
548 portServe := portServes(l)
550 ports := maps.Keys(portServe)
552 for _, port := range ports {
553 srv := portServe[port]
554 for _, ip := range l.IPs {
555 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv)
561func portServes(l config.Listener) map[int]*serve {
562 portServe := map[int]*serve{}
564 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
565 localhost := dns.Domain{ASCII: "localhost"}
567 ldom := l.HostnameDomain
568 if l.Hostname == "" {
569 ldom = mox.Conf.Static.HostnameDomain
571 listenerHostMatch := func(host dns.IPDomain) bool {
575 return host.Domain == ldom || host.Domain == localhost
577 accountHostMatch := func(host dns.IPDomain) bool {
578 if listenerHostMatch(host) {
581 return mox.Conf.IsClientSettingsDomain(host.Domain)
584 var ensureServe func(https bool, port int, kind string, favicon bool) *serve
585 ensureServe = func(https bool, port int, kind string, favicon bool) *serve {
588 s = &serve{nil, nil, false, nil, false, nil}
591 s.Kinds = append(s.Kinds, kind)
592 if favicon && !s.Favicon {
593 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
597 if https && l.TLS.ACME != "" {
598 s.TLSConfig = l.TLS.ACMEConfig
600 s.TLSConfig = l.TLS.Config
601 if l.TLS.ACME != "" {
602 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
603 ensureServe(true, tlsport, "acme-tls-alpn-01", false)
609 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
610 // yet, add http-01 validation mechanism handler to server.
611 ensureACMEHTTP01 := func(srv *serve) {
612 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
613 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
614 srv.Kinds = append(srv.Kinds, "acme-http-01")
615 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
619 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
620 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
621 ensureServe(true, port, "acme-tls-alpn-01", false)
624 if l.AccountHTTP.Enabled {
625 port := config.Port(l.AccountHTTP.Port, 80)
627 if l.AccountHTTP.Path != "" {
628 path = l.AccountHTTP.Path
630 srv := ensureServe(false, port, "account-http at "+path, true)
631 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
632 srv.ServiceHandle("account", accountHostMatch, path, handler)
633 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
634 ensureACMEHTTP01(srv)
636 if l.AccountHTTPS.Enabled {
637 port := config.Port(l.AccountHTTPS.Port, 443)
639 if l.AccountHTTPS.Path != "" {
640 path = l.AccountHTTPS.Path
642 srv := ensureServe(true, port, "account-https at "+path, true)
643 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
644 srv.ServiceHandle("account", accountHostMatch, path, handler)
645 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
648 if l.AdminHTTP.Enabled {
649 port := config.Port(l.AdminHTTP.Port, 80)
651 if l.AdminHTTP.Path != "" {
652 path = l.AdminHTTP.Path
654 srv := ensureServe(false, port, "admin-http at "+path, true)
655 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
656 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
657 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
658 ensureACMEHTTP01(srv)
660 if l.AdminHTTPS.Enabled {
661 port := config.Port(l.AdminHTTPS.Port, 443)
663 if l.AdminHTTPS.Path != "" {
664 path = l.AdminHTTPS.Path
666 srv := ensureServe(true, port, "admin-https at "+path, true)
667 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
668 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
669 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
672 maxMsgSize := l.SMTPMaxMessageSize
674 maxMsgSize = config.DefaultMaxMsgSize
677 if l.WebAPIHTTP.Enabled {
678 port := config.Port(l.WebAPIHTTP.Port, 80)
680 if l.WebAPIHTTP.Path != "" {
681 path = l.WebAPIHTTP.Path
683 srv := ensureServe(false, port, "webapi-http at "+path, true)
684 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
685 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
686 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
687 ensureACMEHTTP01(srv)
689 if l.WebAPIHTTPS.Enabled {
690 port := config.Port(l.WebAPIHTTPS.Port, 443)
692 if l.WebAPIHTTPS.Path != "" {
693 path = l.WebAPIHTTPS.Path
695 srv := ensureServe(true, port, "webapi-https at "+path, true)
696 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
697 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
698 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
701 if l.WebmailHTTP.Enabled {
702 port := config.Port(l.WebmailHTTP.Port, 80)
704 if l.WebmailHTTP.Path != "" {
705 path = l.WebmailHTTP.Path
707 srv := ensureServe(false, port, "webmail-http at "+path, true)
708 var accountPath string
709 if l.AccountHTTP.Enabled {
711 if l.AccountHTTP.Path != "" {
712 accountPath = l.AccountHTTP.Path
715 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
716 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
717 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
718 ensureACMEHTTP01(srv)
720 if l.WebmailHTTPS.Enabled {
721 port := config.Port(l.WebmailHTTPS.Port, 443)
723 if l.WebmailHTTPS.Path != "" {
724 path = l.WebmailHTTPS.Path
726 srv := ensureServe(true, port, "webmail-https at "+path, true)
727 var accountPath string
728 if l.AccountHTTPS.Enabled {
730 if l.AccountHTTPS.Path != "" {
731 accountPath = l.AccountHTTPS.Path
734 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
735 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
736 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
739 if l.MetricsHTTP.Enabled {
740 port := config.Port(l.MetricsHTTP.Port, 8010)
741 srv := ensureServe(false, port, "metrics-http", false)
742 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
743 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
744 if r.URL.Path != "/" {
747 } else if r.Method != "GET" {
748 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
751 w.Header().Set("Content-Type", "text/html")
752 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
755 if l.AutoconfigHTTPS.Enabled {
756 port := config.Port(l.AutoconfigHTTPS.Port, 443)
757 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https", false)
758 if l.AutoconfigHTTPS.NonTLS {
759 ensureACMEHTTP01(srv)
761 autoconfigMatch := func(ipdom dns.IPDomain) bool {
766 // Thunderbird requests an autodiscovery URL at the email address domain name, so
767 // autoconfig prefix is optional.
768 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
769 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
770 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
772 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
773 // use the mail server's host name.
774 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
777 dc, ok := mox.Conf.Domain(dom)
778 return ok && !dc.ReportsOnly
780 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
781 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
782 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
783 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
785 if l.MTASTSHTTPS.Enabled {
786 port := config.Port(l.MTASTSHTTPS.Port, 443)
787 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https", false)
788 if l.MTASTSHTTPS.NonTLS {
789 ensureACMEHTTP01(srv)
791 mtastsMatch := func(ipdom dns.IPDomain) bool {
792 // todo: may want to check this against the configured domains, could in theory be just a webserver.
797 return strings.HasPrefix(dom.ASCII, "mta-sts.")
799 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
801 if l.PprofHTTP.Enabled {
802 // Importing net/http/pprof registers handlers on the default serve mux.
803 port := config.Port(l.PprofHTTP.Port, 8011)
804 if _, ok := portServe[port]; ok {
805 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
807 srv := &serve{[]string{"pprof-http"}, nil, false, nil, false, nil}
808 portServe[port] = srv
809 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
811 if l.WebserverHTTP.Enabled {
812 port := config.Port(l.WebserverHTTP.Port, 80)
813 srv := ensureServe(false, port, "webserver-http", false)
815 ensureACMEHTTP01(srv)
817 if l.WebserverHTTPS.Enabled {
818 port := config.Port(l.WebserverHTTPS.Port, 443)
819 srv := ensureServe(true, port, "webserver-https", false)
823 if l.TLS != nil && l.TLS.ACME != "" {
824 hosts := map[dns.Domain]struct{}{
825 mox.Conf.Static.HostnameDomain: {},
827 if l.HostnameDomain.ASCII != "" {
828 hosts[l.HostnameDomain] = struct{}{}
830 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
831 // presence of TLS certificates for.
832 for _, name := range mox.Conf.Domains() {
833 if dom, err := dns.ParseDomain(name); err != nil {
834 pkglog.Errorx("parsing domain from config", err)
835 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
836 // Do not gather autoconfig name if we aren't accepting email for this domain.
840 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
842 pkglog.Errorx("parsing domain from config for autoconfig", err)
844 hosts[autoconfdom] = struct{}{}
848 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
849 ensureManagerHosts[m] = hosts
852 for _, srv := range portServe {
853 sortPathHandlers(srv.SystemHandlers)
854 sortPathHandlers(srv.ServiceHandlers)
860func sortPathHandlers(l []pathHandler) {
861 sort.Slice(l, func(i, j int) bool {
864 if len(a) == len(b) {
865 // For consistent order.
868 // Longest paths first.
869 return len(a) > len(b)
873// functions to be launched in goroutine that will serve on a listener.
876// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
877// immediately after startup. We only do so for our explicit listener hostnames,
878// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
879// do request autoconfig, otherwise clients may run into their timeouts waiting for
880// the certificate to be given during the first https connection.
881var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
883// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
884func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler) {
885 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
890 if tlsConfig == nil {
892 if os.Getuid() == 0 {
893 pkglog.Print("http listener",
894 slog.String("name", name),
895 slog.String("kinds", strings.Join(kinds, ",")),
896 slog.String("address", addr))
898 ln, err = mox.Listen(mox.Network(ip), addr)
900 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
904 if os.Getuid() == 0 {
905 pkglog.Print("https listener",
906 slog.String("name", name),
907 slog.String("kinds", strings.Join(kinds, ",")),
908 slog.String("address", addr))
910 ln, err = mox.Listen(mox.Network(ip), addr)
912 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
914 ln = tls.NewListener(ln, tlsConfig)
917 server := &http.Server{
919 // Clone because our multiple Server.Serve calls modify config concurrently leading to data race.
920 TLSConfig: tlsConfig.Clone(),
921 ReadHeaderTimeout: 30 * time.Second,
922 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
923 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
926 err := server.Serve(ln)
927 pkglog.Fatalx(protocol+": serve", err)
929 servers = append(servers, serve)
932// Serve starts serving on the initialized listeners.
934 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
936 go webaccount.ImportManage()
938 for _, serve := range servers {
944 time.Sleep(1 * time.Second)
946 for m, hosts := range ensureManagerHosts {
947 for host := range hosts {
948 // Check if certificate is already available. If so, we don't print as much after a
949 // restart, and finish more quickly if only a few certificates are missing/old.
950 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
951 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
957 // Just in case someone adds quite some domains to their config. We don't want to
958 // hit any ACME rate limits.
962 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
963 time.Sleep(10 * time.Second)
967 hello := &tls.ClientHelloInfo{
968 ServerName: host.ASCII,
970 // Make us fetch an ECDSA P256 cert.
971 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
972 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
973 SupportedCurves: []tls.CurveID{tls.CurveP256},
974 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
975 SupportedVersions: []uint16{tls.VersionTLS13},
977 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
978 if _, err := m.Manager.GetCertificate(hello); err != nil {
979 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))