1// Package http provides HTTP listeners/servers, for
2// autoconfiguration/autodiscovery, the account and admin web interface and
26 "golang.org/x/exp/maps"
27 "golang.org/x/net/http2"
29 "github.com/prometheus/client_golang/prometheus"
30 "github.com/prometheus/client_golang/prometheus/promauto"
31 "github.com/prometheus/client_golang/prometheus/promhttp"
33 "github.com/mjl-/mox/autotls"
34 "github.com/mjl-/mox/config"
35 "github.com/mjl-/mox/dns"
36 "github.com/mjl-/mox/imapserver"
37 "github.com/mjl-/mox/mlog"
38 "github.com/mjl-/mox/mox-"
39 "github.com/mjl-/mox/ratelimit"
40 "github.com/mjl-/mox/smtpserver"
41 "github.com/mjl-/mox/webaccount"
42 "github.com/mjl-/mox/webadmin"
43 "github.com/mjl-/mox/webapisrv"
44 "github.com/mjl-/mox/webmail"
47var pkglog = mlog.New("http", nil)
50 // metricRequest tracks performance (time to write response header) of server.
51 metricRequest = promauto.NewHistogramVec(
52 prometheus.HistogramOpts{
53 Name: "mox_httpserver_request_duration_seconds",
54 Help: "HTTP(s) server request with handler name, protocol, method, result codes, and duration until response status code is written, in seconds.",
55 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
58 "handler", // Name from webhandler, can be empty.
59 "proto", // "http", "https", "ws", "wss"
60 "method", // "(unknown)" and otherwise only common verbs
64 // metricResponse tracks performance of entire request as experienced by users,
65 // which also depends on their connection speed, so not necessarily something you
67 metricResponse = promauto.NewHistogramVec(
68 prometheus.HistogramOpts{
69 Name: "mox_httpserver_response_duration_seconds",
70 Help: "HTTP(s) server response with handler name, protocol, method, result codes, and duration of entire response, in seconds.",
71 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
74 "handler", // Name from webhandler, can be empty.
75 "proto", // "http", "https", "ws", "wss"
76 "method", // "(unknown)" and otherwise only common verbs
82// We serve a favicon when webaccount/webmail/webadmin/webapi for account-related
83// domains. They are configured as "service handler", which have a lower priority
84// than web handler. Admins can configure a custom /favicon.ico route to override
85// the builtin favicon. In the future, we may want to make it easier to customize
86// the favicon, possibly per client settings domain.
90var faviconModTime = time.Now()
93 p, err := os.Executable()
95 if st, err := os.Stat(p); err == nil {
96 faviconModTime = st.ModTime()
101func faviconHandle(w http.ResponseWriter, r *http.Request) {
102 http.ServeContent(w, r, "favicon.ico", faviconModTime, strings.NewReader(faviconIco))
105type responseWriterFlusher interface {
110// http.ResponseWriter that writes access log and tracks metrics at end of response.
111type loggingWriter struct {
112 W responseWriterFlusher // Calls are forwarded.
115 WebsocketRequest bool // Whether request from was websocket.
123 Size int64 // Of data served to client, for non-websocket responses.
124 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
125 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
127 WebsocketResponse bool // If this was a successful websocket connection with backend.
128 SizeFromClient, SizeToClient int64 // Websocket data.
129 Attrs []slog.Attr // Additional fields to log.
132func (w *loggingWriter) AddAttr(a slog.Attr) {
133 w.Attrs = append(w.Attrs, a)
136func (w *loggingWriter) Flush() {
140func (w *loggingWriter) Header() http.Header {
144// protocol, for logging.
145func (w *loggingWriter) proto(websocket bool) string {
156func (w *loggingWriter) Write(buf []byte) (int, error) {
157 if w.StatusCode == 0 {
158 w.WriteHeader(http.StatusOK)
164 n, err = w.W.Write(buf)
169 // We flush after each write. Probably takes a few more bytes, but prevents any
170 // issues due to buffering.
171 // w.Gzip.Write updates w.Size with the compressed byte count.
172 n, err = w.Gzip.Write(buf)
177 w.UncompressedSize += int64(n)
186func (w *loggingWriter) setStatusCode(statusCode int) {
187 if w.StatusCode != 0 {
191 w.StatusCode = statusCode
192 method := metricHTTPMethod(w.R.Method)
193 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
196// SetUncompressedSize is used through an interface by
197// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
198func (w *loggingWriter) SetUncompressedSize(origSize int64) {
199 w.UncompressedSize = origSize
202func (w *loggingWriter) WriteHeader(statusCode int) {
203 if w.StatusCode != 0 {
207 w.setStatusCode(statusCode)
209 // We transparently gzip-compress responses for requests under these conditions, all must apply:
211 // - Enabled for handler (static handlers make their own decisions).
212 // - Not a websocket request.
213 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
214 // - Not already compressed, or any other Content-Encoding header (including "identity").
215 // - Client accepts gzip encoded responses.
216 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
217 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")) {
218 // 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.
220 // We track the gzipped output for the access log.
221 cw := countWriter{Writer: w.W, Size: &w.Size}
222 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
223 w.W.Header().Set("Content-Encoding", "gzip")
224 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
226 w.W.WriteHeader(statusCode)
229func acceptsGzip(r *http.Request) bool {
230 s := r.Header.Get("Accept-Encoding")
231 t := strings.Split(s, ",")
232 for _, e := range t {
233 e = strings.TrimSpace(e)
234 tt := strings.Split(e, ";")
235 if len(tt) > 1 && t[1] == "q=0" {
245var compressibleTypes = map[string]bool{
246 "application/csv": true,
247 "application/javascript": true,
248 "application/json": true,
249 "application/x-javascript": true,
250 "application/xml": true,
251 "image/vnd.microsoft.icon": true,
252 "image/x-icon": true,
256 "font/opentype": true,
259func compressibleContentType(ct string) bool {
260 ct = strings.SplitN(ct, ";", 2)[0]
261 ct = strings.TrimSpace(ct)
262 ct = strings.ToLower(ct)
263 if compressibleTypes[ct] {
266 t, st, _ := strings.Cut(ct, "/")
267 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
270func compressibleContent(f *os.File) bool {
271 // We don't want to store many small files. They take up too much disk overhead.
272 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
276 buf := make([]byte, 512)
277 n, err := f.ReadAt(buf, 0)
278 if err != nil && err != io.EOF {
281 ct := http.DetectContentType(buf[:n])
282 return compressibleContentType(ct)
285type countWriter struct {
290func (w countWriter) Write(buf []byte) (int, error) {
291 n, err := w.Writer.Write(buf)
298var tlsVersions = map[uint16]string{
299 tls.VersionTLS10: "tls1.0",
300 tls.VersionTLS11: "tls1.1",
301 tls.VersionTLS12: "tls1.2",
302 tls.VersionTLS13: "tls1.3",
305func metricHTTPMethod(method string) string {
306 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
307 method = strings.ToLower(method)
309 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":
315func (w *loggingWriter) error(err error) {
321func (w *loggingWriter) Done() {
322 if w.Err == nil && w.Gzip != nil {
323 if err := w.Gzip.Close(); err != nil {
328 method := metricHTTPMethod(w.R.Method)
329 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
333 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
341 err = w.R.Context().Err()
343 attrs := []slog.Attr{
344 slog.String("httpaccess", ""),
345 slog.String("handler", w.Handler),
346 slog.String("method", method),
347 slog.Any("url", w.R.URL),
348 slog.String("host", w.R.Host),
349 slog.Duration("duration", time.Since(w.Start)),
350 slog.Int("statuscode", w.StatusCode),
351 slog.String("proto", strings.ToLower(w.R.Proto)),
352 slog.Any("remoteaddr", w.R.RemoteAddr),
353 slog.String("tlsinfo", tlsinfo),
354 slog.String("useragent", w.R.Header.Get("User-Agent")),
355 slog.String("referrr", w.R.Header.Get("Referrer")),
357 if w.WebsocketRequest {
358 attrs = append(attrs,
359 slog.Bool("websocketrequest", true),
362 if w.WebsocketResponse {
363 attrs = append(attrs,
364 slog.Bool("websocket", true),
365 slog.Int64("sizetoclient", w.SizeToClient),
366 slog.Int64("sizefromclient", w.SizeFromClient),
368 } else if w.UncompressedSize > 0 {
369 attrs = append(attrs,
370 slog.Int64("size", w.Size),
371 slog.Int64("uncompressedsize", w.UncompressedSize),
374 attrs = append(attrs,
375 slog.Int64("size", w.Size),
378 attrs = append(attrs, w.Attrs...)
379 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
382// Built-in handlers, e.g. mta-sts and autoconfig.
383type pathHandler struct {
384 Name string // For logging/metrics.
385 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.
386 Path string // Path to register, like on http.ServeMux.
390 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https).
391 TLSConfig *tls.Config
392 NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port.
395 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
396 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
397 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
398 // interfaces) last. WebHandlers can also pass requests to the internal servers.
399 // This order allows admins to serve other content on domains serving the mox.conf
400 // internal services.
401 SystemHandlers []pathHandler // Sorted, longest first.
403 ServiceHandlers []pathHandler // Sorted, longest first.
406// SystemHandle registers a named system handler for a path and optional host. If
407// path ends with a slash, it is used as prefix match, otherwise a full path match
408// is required. If hostOpt is set, only requests to those host are handled by this
410func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
411 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
414// Like SystemHandle, but for internal services "admin", "account", "webmail",
415// "webapi" configured in the mox.conf Listener.
416func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
417 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
421 limiterConnectionrate = &ratelimit.Limiter{
422 WindowLimits: []ratelimit.WindowLimit{
425 Limits: [...]int64{1000, 3000, 9000},
429 Limits: [...]int64{5000, 15000, 45000},
435// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
436// right pathHandler or WebHandler, and it generates access logs and tracks
438func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
440 // Rate limiting as early as possible.
441 ipstr, _, err := net.SplitHostPort(r.RemoteAddr)
443 pkglog.Debugx("split host:port client remoteaddr", err, slog.Any("remoteaddr", r.RemoteAddr))
444 } else if ip := net.ParseIP(ipstr); ip == nil {
445 pkglog.Debug("parsing ip for client remoteaddr", slog.Any("remoteaddr", r.RemoteAddr))
446 } else if !limiterConnectionrate.Add(ip, now, 1) {
447 method := metricHTTPMethod(r.Method)
452 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
453 // No logging, that's just noise.
455 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
459 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
460 r = r.WithContext(ctx)
462 wf, ok := xw.(responseWriterFlusher)
464 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
468 nw := &loggingWriter{
475 // Cleanup path, removing ".." and ".". Keep any trailing slash.
476 trailingPath := strings.HasSuffix(r.URL.Path, "/")
477 if r.URL.Path == "" {
480 r.URL.Path = path.Clean(r.URL.Path)
481 if r.URL.Path == "." {
484 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
489 nhost, _, err := net.SplitHostPort(host)
493 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
495 dom, domErr := dns.ParseDomain(host)
497 ipdom = dns.IPDomain{Domain: dom}
501 handle := func(h pathHandler) bool {
502 if h.HostMatch != nil && !h.HostMatch(ipdom) {
505 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
508 h.Handler.ServeHTTP(nw, r)
514 for _, h := range s.SystemHandlers {
520 if WebHandle(nw, r, ipdom) {
524 for _, h := range s.ServiceHandlers {
529 nw.Handler = "(nomatch)"
533func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
534 // Helpfully redirect user to version with ending slash.
535 if path != "/" && strings.HasSuffix(path, "/") {
536 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
537 http.Redirect(w, r, path, http.StatusSeeOther)
539 srv.ServiceHandle(name, hostMatch, path[:len(path)-1], handler)
543// Listen binds to sockets for HTTP listeners, including those required for ACME to
544// generate TLS certificates. It stores the listeners so Serve can start serving them.
546 // Initialize listeners in deterministic order for the same potential error
548 names := maps.Keys(mox.Conf.Static.Listeners)
550 for _, name := range names {
551 l := mox.Conf.Static.Listeners[name]
552 portServe := portServes(name, l)
554 ports := maps.Keys(portServe)
556 for _, port := range ports {
557 srv := portServe[port]
558 for _, ip := range l.IPs {
559 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, srv.NextProto)
565func portServes(name string, l config.Listener) map[int]*serve {
566 portServe := map[int]*serve{}
568 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
569 localhost := dns.Domain{ASCII: "localhost"}
571 ldom := l.HostnameDomain
572 if l.Hostname == "" {
573 ldom = mox.Conf.Static.HostnameDomain
575 listenerHostMatch := func(host dns.IPDomain) bool {
579 return host.Domain == ldom || host.Domain == localhost
581 accountHostMatch := func(host dns.IPDomain) bool {
582 if listenerHostMatch(host) {
585 return mox.Conf.IsClientSettingsDomain(host.Domain)
588 var ensureServe func(https bool, port int, kind string, favicon bool) *serve
589 ensureServe = func(https bool, port int, kind string, favicon bool) *serve {
592 s = &serve{nil, nil, tlsNextProtoMap{}, false, nil, false, nil}
595 s.Kinds = append(s.Kinds, kind)
596 if favicon && !s.Favicon {
597 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
601 // We clone TLS configs because we may modify it later on for this server, for
602 // ALPN. And we need copies because multiple listeners on http.Server where the
603 // config is used will try to modify it concurrently.
604 if https && l.TLS.ACME != "" {
605 s.TLSConfig = l.TLS.ACMEConfig.Clone()
607 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
608 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
609 ensureServe(true, tlsport, "acme-tls-alpn-01", false)
612 s.TLSConfig = l.TLS.Config.Clone()
617 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
618 // yet, add http-01 validation mechanism handler to server.
619 ensureACMEHTTP01 := func(srv *serve) {
620 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
621 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
622 srv.Kinds = append(srv.Kinds, "acme-http-01")
623 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
627 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
628 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
629 ensureServe(true, port, "acme-tls-alpn-01", false)
631 if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
632 s := ensureServe(true, 443, "smtp-https", false)
633 hostname := mox.Conf.Static.HostnameDomain
634 if l.Hostname != "" {
635 hostname = l.HostnameDomain
638 maxMsgSize := l.SMTPMaxMessageSize
640 maxMsgSize = config.DefaultMaxMsgSize
642 requireTLS := !l.SMTP.NoRequireTLS
644 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
645 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
648 if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
649 s := ensureServe(true, 443, "imap-https", false)
650 s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
651 imapserver.ServeTLSConn(name, conn, s.TLSConfig)
654 if l.AccountHTTP.Enabled {
655 port := config.Port(l.AccountHTTP.Port, 80)
657 if l.AccountHTTP.Path != "" {
658 path = l.AccountHTTP.Path
660 srv := ensureServe(false, port, "account-http at "+path, true)
661 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
662 srv.ServiceHandle("account", accountHostMatch, path, handler)
663 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
664 ensureACMEHTTP01(srv)
666 if l.AccountHTTPS.Enabled {
667 port := config.Port(l.AccountHTTPS.Port, 443)
669 if l.AccountHTTPS.Path != "" {
670 path = l.AccountHTTPS.Path
672 srv := ensureServe(true, port, "account-https at "+path, true)
673 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
674 srv.ServiceHandle("account", accountHostMatch, path, handler)
675 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
678 if l.AdminHTTP.Enabled {
679 port := config.Port(l.AdminHTTP.Port, 80)
681 if l.AdminHTTP.Path != "" {
682 path = l.AdminHTTP.Path
684 srv := ensureServe(false, port, "admin-http at "+path, true)
685 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
686 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
687 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
688 ensureACMEHTTP01(srv)
690 if l.AdminHTTPS.Enabled {
691 port := config.Port(l.AdminHTTPS.Port, 443)
693 if l.AdminHTTPS.Path != "" {
694 path = l.AdminHTTPS.Path
696 srv := ensureServe(true, port, "admin-https at "+path, true)
697 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
698 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
699 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
702 maxMsgSize := l.SMTPMaxMessageSize
704 maxMsgSize = config.DefaultMaxMsgSize
707 if l.WebAPIHTTP.Enabled {
708 port := config.Port(l.WebAPIHTTP.Port, 80)
710 if l.WebAPIHTTP.Path != "" {
711 path = l.WebAPIHTTP.Path
713 srv := ensureServe(false, port, "webapi-http at "+path, true)
714 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
715 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
716 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
717 ensureACMEHTTP01(srv)
719 if l.WebAPIHTTPS.Enabled {
720 port := config.Port(l.WebAPIHTTPS.Port, 443)
722 if l.WebAPIHTTPS.Path != "" {
723 path = l.WebAPIHTTPS.Path
725 srv := ensureServe(true, port, "webapi-https at "+path, true)
726 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
727 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
728 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
731 if l.WebmailHTTP.Enabled {
732 port := config.Port(l.WebmailHTTP.Port, 80)
734 if l.WebmailHTTP.Path != "" {
735 path = l.WebmailHTTP.Path
737 srv := ensureServe(false, port, "webmail-http at "+path, true)
738 var accountPath string
739 if l.AccountHTTP.Enabled {
741 if l.AccountHTTP.Path != "" {
742 accountPath = l.AccountHTTP.Path
745 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
746 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
747 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
748 ensureACMEHTTP01(srv)
750 if l.WebmailHTTPS.Enabled {
751 port := config.Port(l.WebmailHTTPS.Port, 443)
753 if l.WebmailHTTPS.Path != "" {
754 path = l.WebmailHTTPS.Path
756 srv := ensureServe(true, port, "webmail-https at "+path, true)
757 var accountPath string
758 if l.AccountHTTPS.Enabled {
760 if l.AccountHTTPS.Path != "" {
761 accountPath = l.AccountHTTPS.Path
764 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
765 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
766 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
769 if l.MetricsHTTP.Enabled {
770 port := config.Port(l.MetricsHTTP.Port, 8010)
771 srv := ensureServe(false, port, "metrics-http", false)
772 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
773 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
774 if r.URL.Path != "/" {
777 } else if r.Method != "GET" {
778 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
781 w.Header().Set("Content-Type", "text/html")
782 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
785 if l.AutoconfigHTTPS.Enabled {
786 port := config.Port(l.AutoconfigHTTPS.Port, 443)
787 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https", false)
788 if l.AutoconfigHTTPS.NonTLS {
789 ensureACMEHTTP01(srv)
791 autoconfigMatch := func(ipdom dns.IPDomain) bool {
796 // Thunderbird requests an autodiscovery URL at the email address domain name, so
797 // autoconfig prefix is optional.
798 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
799 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
800 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
802 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
803 // use the mail server's host name.
804 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
807 dc, ok := mox.Conf.Domain(dom)
808 return ok && !dc.ReportsOnly
810 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
811 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
812 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
813 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
815 if l.MTASTSHTTPS.Enabled {
816 port := config.Port(l.MTASTSHTTPS.Port, 443)
817 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https", false)
818 if l.MTASTSHTTPS.NonTLS {
819 ensureACMEHTTP01(srv)
821 mtastsMatch := func(ipdom dns.IPDomain) bool {
822 // todo: may want to check this against the configured domains, could in theory be just a webserver.
827 return strings.HasPrefix(dom.ASCII, "mta-sts.")
829 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
831 if l.PprofHTTP.Enabled {
832 // Importing net/http/pprof registers handlers on the default serve mux.
833 port := config.Port(l.PprofHTTP.Port, 8011)
834 if _, ok := portServe[port]; ok {
835 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
837 srv := &serve{[]string{"pprof-http"}, nil, nil, false, nil, false, nil}
838 portServe[port] = srv
839 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
841 if l.WebserverHTTP.Enabled {
842 port := config.Port(l.WebserverHTTP.Port, 80)
843 srv := ensureServe(false, port, "webserver-http", false)
845 ensureACMEHTTP01(srv)
847 if l.WebserverHTTPS.Enabled {
848 port := config.Port(l.WebserverHTTPS.Port, 443)
849 srv := ensureServe(true, port, "webserver-https", false)
853 if l.TLS != nil && l.TLS.ACME != "" {
854 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
855 if ensureManagerHosts[m] == nil {
856 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
858 hosts := ensureManagerHosts[m]
859 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
861 if l.HostnameDomain.ASCII != "" {
862 hosts[l.HostnameDomain] = struct{}{}
865 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
866 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
867 // for the timeouts of clients doing autoconfig.
869 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
870 for _, name := range mox.Conf.Domains() {
871 if dom, err := dns.ParseDomain(name); err != nil {
872 pkglog.Errorx("parsing domain from config", err)
873 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly || d.Disabled {
874 // Do not gather autoconfig name if we aren't accepting email for this domain or when it is disabled.
878 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
880 pkglog.Errorx("parsing domain from config for autoconfig", err)
882 hosts[autoconfdom] = struct{}{}
888 if s := portServe[443]; s != nil && s.TLSConfig != nil && len(s.NextProto) > 0 {
889 s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, maps.Keys(s.NextProto)...)
892 for _, srv := range portServe {
893 sortPathHandlers(srv.SystemHandlers)
894 sortPathHandlers(srv.ServiceHandlers)
900func sortPathHandlers(l []pathHandler) {
901 sort.Slice(l, func(i, j int) bool {
904 if len(a) == len(b) {
905 // For consistent order.
908 // Longest paths first.
909 return len(a) > len(b)
913// functions to be launched in goroutine that will serve on a listener.
916// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
917// immediately after startup. We only do so for our explicit listener hostnames,
918// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
919// do request autoconfig, otherwise clients may run into their timeouts waiting for
920// the certificate to be given during the first https connection.
921var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
923type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
925// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
926func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, nextProto tlsNextProtoMap) {
927 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
932 if tlsConfig == nil {
934 if os.Getuid() == 0 {
935 pkglog.Print("http listener",
936 slog.String("name", name),
937 slog.String("kinds", strings.Join(kinds, ",")),
938 slog.String("address", addr))
940 ln, err = mox.Listen(mox.Network(ip), addr)
942 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
946 if os.Getuid() == 0 {
947 pkglog.Print("https listener",
948 slog.String("name", name),
949 slog.String("kinds", strings.Join(kinds, ",")),
950 slog.String("address", addr))
952 ln, err = mox.Listen(mox.Network(ip), addr)
954 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
956 ln = tls.NewListener(ln, tlsConfig)
959 server := &http.Server{
961 TLSConfig: tlsConfig,
962 ReadHeaderTimeout: 30 * time.Second,
963 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
964 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
965 TLSNextProto: nextProto,
967 // By default, the Go 1.6 and above http.Server includes support for HTTP2.
968 // However, HTTP2 is negotiated via ALPN. Because we are configuring
969 // TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
970 // and calling ConfigureServer.
971 err = http2.ConfigureServer(server, nil)
973 pkglog.Fatalx("https: unable to configure http2", err)
976 err := server.Serve(ln)
977 pkglog.Fatalx(protocol+": serve", err)
979 servers = append(servers, serve)
982// Serve starts serving on the initialized listeners.
984 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
986 go webaccount.ImportManage()
988 for _, serve := range servers {
994 time.Sleep(1 * time.Second)
996 for m, hosts := range ensureManagerHosts {
997 for host := range hosts {
998 // Check if certificate is already available. If so, we don't print as much after a
999 // restart, and finish more quickly if only a few certificates are missing/old.
1000 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
1001 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
1007 // Just in case someone adds quite some domains to their config. We don't want to
1008 // hit any ACME rate limits.
1012 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
1013 time.Sleep(10 * time.Second)
1017 hello := &tls.ClientHelloInfo{
1018 ServerName: host.ASCII,
1020 // Make us fetch an ECDSA P256 cert.
1021 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
1022 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
1023 SupportedCurves: []tls.CurveID{tls.CurveP256},
1024 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
1025 SupportedVersions: []uint16{tls.VersionTLS13},
1027 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
1028 if _, err := m.Manager.GetCertificate(hello); err != nil {
1029 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))