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 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
601 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
602 ensureServe(true, tlsport, "acme-tls-alpn-01", false)
605 s.TLSConfig = l.TLS.Config
610 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
611 // yet, add http-01 validation mechanism handler to server.
612 ensureACMEHTTP01 := func(srv *serve) {
613 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
614 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
615 srv.Kinds = append(srv.Kinds, "acme-http-01")
616 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
620 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
621 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
622 ensureServe(true, port, "acme-tls-alpn-01", false)
625 if l.AccountHTTP.Enabled {
626 port := config.Port(l.AccountHTTP.Port, 80)
628 if l.AccountHTTP.Path != "" {
629 path = l.AccountHTTP.Path
631 srv := ensureServe(false, port, "account-http at "+path, true)
632 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
633 srv.ServiceHandle("account", accountHostMatch, path, handler)
634 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
635 ensureACMEHTTP01(srv)
637 if l.AccountHTTPS.Enabled {
638 port := config.Port(l.AccountHTTPS.Port, 443)
640 if l.AccountHTTPS.Path != "" {
641 path = l.AccountHTTPS.Path
643 srv := ensureServe(true, port, "account-https at "+path, true)
644 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
645 srv.ServiceHandle("account", accountHostMatch, path, handler)
646 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
649 if l.AdminHTTP.Enabled {
650 port := config.Port(l.AdminHTTP.Port, 80)
652 if l.AdminHTTP.Path != "" {
653 path = l.AdminHTTP.Path
655 srv := ensureServe(false, port, "admin-http at "+path, true)
656 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
657 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
658 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
659 ensureACMEHTTP01(srv)
661 if l.AdminHTTPS.Enabled {
662 port := config.Port(l.AdminHTTPS.Port, 443)
664 if l.AdminHTTPS.Path != "" {
665 path = l.AdminHTTPS.Path
667 srv := ensureServe(true, port, "admin-https at "+path, true)
668 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
669 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
670 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
673 maxMsgSize := l.SMTPMaxMessageSize
675 maxMsgSize = config.DefaultMaxMsgSize
678 if l.WebAPIHTTP.Enabled {
679 port := config.Port(l.WebAPIHTTP.Port, 80)
681 if l.WebAPIHTTP.Path != "" {
682 path = l.WebAPIHTTP.Path
684 srv := ensureServe(false, port, "webapi-http at "+path, true)
685 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
686 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
687 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
688 ensureACMEHTTP01(srv)
690 if l.WebAPIHTTPS.Enabled {
691 port := config.Port(l.WebAPIHTTPS.Port, 443)
693 if l.WebAPIHTTPS.Path != "" {
694 path = l.WebAPIHTTPS.Path
696 srv := ensureServe(true, port, "webapi-https at "+path, true)
697 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
698 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
699 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
702 if l.WebmailHTTP.Enabled {
703 port := config.Port(l.WebmailHTTP.Port, 80)
705 if l.WebmailHTTP.Path != "" {
706 path = l.WebmailHTTP.Path
708 srv := ensureServe(false, port, "webmail-http at "+path, true)
709 var accountPath string
710 if l.AccountHTTP.Enabled {
712 if l.AccountHTTP.Path != "" {
713 accountPath = l.AccountHTTP.Path
716 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
717 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
718 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
719 ensureACMEHTTP01(srv)
721 if l.WebmailHTTPS.Enabled {
722 port := config.Port(l.WebmailHTTPS.Port, 443)
724 if l.WebmailHTTPS.Path != "" {
725 path = l.WebmailHTTPS.Path
727 srv := ensureServe(true, port, "webmail-https at "+path, true)
728 var accountPath string
729 if l.AccountHTTPS.Enabled {
731 if l.AccountHTTPS.Path != "" {
732 accountPath = l.AccountHTTPS.Path
735 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
736 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
737 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
740 if l.MetricsHTTP.Enabled {
741 port := config.Port(l.MetricsHTTP.Port, 8010)
742 srv := ensureServe(false, port, "metrics-http", false)
743 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
744 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
745 if r.URL.Path != "/" {
748 } else if r.Method != "GET" {
749 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
752 w.Header().Set("Content-Type", "text/html")
753 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
756 if l.AutoconfigHTTPS.Enabled {
757 port := config.Port(l.AutoconfigHTTPS.Port, 443)
758 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https", false)
759 if l.AutoconfigHTTPS.NonTLS {
760 ensureACMEHTTP01(srv)
762 autoconfigMatch := func(ipdom dns.IPDomain) bool {
767 // Thunderbird requests an autodiscovery URL at the email address domain name, so
768 // autoconfig prefix is optional.
769 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
770 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
771 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
773 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
774 // use the mail server's host name.
775 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
778 dc, ok := mox.Conf.Domain(dom)
779 return ok && !dc.ReportsOnly
781 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
782 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
783 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
784 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
786 if l.MTASTSHTTPS.Enabled {
787 port := config.Port(l.MTASTSHTTPS.Port, 443)
788 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https", false)
789 if l.MTASTSHTTPS.NonTLS {
790 ensureACMEHTTP01(srv)
792 mtastsMatch := func(ipdom dns.IPDomain) bool {
793 // todo: may want to check this against the configured domains, could in theory be just a webserver.
798 return strings.HasPrefix(dom.ASCII, "mta-sts.")
800 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
802 if l.PprofHTTP.Enabled {
803 // Importing net/http/pprof registers handlers on the default serve mux.
804 port := config.Port(l.PprofHTTP.Port, 8011)
805 if _, ok := portServe[port]; ok {
806 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
808 srv := &serve{[]string{"pprof-http"}, nil, false, nil, false, nil}
809 portServe[port] = srv
810 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
812 if l.WebserverHTTP.Enabled {
813 port := config.Port(l.WebserverHTTP.Port, 80)
814 srv := ensureServe(false, port, "webserver-http", false)
816 ensureACMEHTTP01(srv)
818 if l.WebserverHTTPS.Enabled {
819 port := config.Port(l.WebserverHTTPS.Port, 443)
820 srv := ensureServe(true, port, "webserver-https", false)
824 if l.TLS != nil && l.TLS.ACME != "" {
825 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
826 if ensureManagerHosts[m] == nil {
827 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
829 hosts := ensureManagerHosts[m]
830 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
832 if l.HostnameDomain.ASCII != "" {
833 hosts[l.HostnameDomain] = struct{}{}
836 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
837 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
838 // for the timeouts of clients doing autoconfig.
840 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
841 for _, name := range mox.Conf.Domains() {
842 if dom, err := dns.ParseDomain(name); err != nil {
843 pkglog.Errorx("parsing domain from config", err)
844 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
845 // Do not gather autoconfig name if we aren't accepting email for this domain.
849 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
851 pkglog.Errorx("parsing domain from config for autoconfig", err)
853 hosts[autoconfdom] = struct{}{}
859 for _, srv := range portServe {
860 sortPathHandlers(srv.SystemHandlers)
861 sortPathHandlers(srv.ServiceHandlers)
867func sortPathHandlers(l []pathHandler) {
868 sort.Slice(l, func(i, j int) bool {
871 if len(a) == len(b) {
872 // For consistent order.
875 // Longest paths first.
876 return len(a) > len(b)
880// functions to be launched in goroutine that will serve on a listener.
883// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
884// immediately after startup. We only do so for our explicit listener hostnames,
885// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
886// do request autoconfig, otherwise clients may run into their timeouts waiting for
887// the certificate to be given during the first https connection.
888var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
890// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
891func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler) {
892 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
897 if tlsConfig == nil {
899 if os.Getuid() == 0 {
900 pkglog.Print("http listener",
901 slog.String("name", name),
902 slog.String("kinds", strings.Join(kinds, ",")),
903 slog.String("address", addr))
905 ln, err = mox.Listen(mox.Network(ip), addr)
907 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
911 if os.Getuid() == 0 {
912 pkglog.Print("https listener",
913 slog.String("name", name),
914 slog.String("kinds", strings.Join(kinds, ",")),
915 slog.String("address", addr))
917 ln, err = mox.Listen(mox.Network(ip), addr)
919 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
921 ln = tls.NewListener(ln, tlsConfig)
924 server := &http.Server{
926 // Clone because our multiple Server.Serve calls modify config concurrently leading to data race.
927 TLSConfig: tlsConfig.Clone(),
928 ReadHeaderTimeout: 30 * time.Second,
929 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
930 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
933 err := server.Serve(ln)
934 pkglog.Fatalx(protocol+": serve", err)
936 servers = append(servers, serve)
939// Serve starts serving on the initialized listeners.
941 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
943 go webaccount.ImportManage()
945 for _, serve := range servers {
951 time.Sleep(1 * time.Second)
953 for m, hosts := range ensureManagerHosts {
954 for host := range hosts {
955 // Check if certificate is already available. If so, we don't print as much after a
956 // restart, and finish more quickly if only a few certificates are missing/old.
957 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
958 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
964 // Just in case someone adds quite some domains to their config. We don't want to
965 // hit any ACME rate limits.
969 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
970 time.Sleep(10 * time.Second)
974 hello := &tls.ClientHelloInfo{
975 ServerName: host.ASCII,
977 // Make us fetch an ECDSA P256 cert.
978 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
979 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
980 SupportedCurves: []tls.CurveID{tls.CurveP256},
981 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
982 SupportedVersions: []uint16{tls.VersionTLS13},
984 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
985 if _, err := m.Manager.GetCertificate(hello); err != nil {
986 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))