1// Package http provides HTTP listeners/servers, for
2// autoconfiguration/autodiscovery, the account and admin web interface and
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.
116 WebsocketRequest bool // Whether request from was websocket.
124 Size int64 // Of data served to client, for non-websocket responses.
125 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
126 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
128 WebsocketResponse bool // If this was a successful websocket connection with backend.
129 SizeFromClient, SizeToClient int64 // Websocket data.
130 Attrs []slog.Attr // Additional fields to log.
133func (w *loggingWriter) AddAttr(a slog.Attr) {
134 w.Attrs = append(w.Attrs, a)
137func (w *loggingWriter) Flush() {
141func (w *loggingWriter) Header() http.Header {
145// protocol, for logging.
146func (w *loggingWriter) proto(websocket bool) string {
157func (w *loggingWriter) Write(buf []byte) (int, error) {
158 if w.StatusCode == 0 {
159 w.WriteHeader(http.StatusOK)
165 n, err = w.W.Write(buf)
170 // We flush after each write. Probably takes a few more bytes, but prevents any
171 // issues due to buffering.
172 // w.Gzip.Write updates w.Size with the compressed byte count.
173 n, err = w.Gzip.Write(buf)
178 w.UncompressedSize += int64(n)
187func (w *loggingWriter) setStatusCode(statusCode int) {
188 if w.StatusCode != 0 {
192 w.StatusCode = statusCode
193 method := metricHTTPMethod(w.R.Method)
194 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
197// SetUncompressedSize is used through an interface by
198// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
199func (w *loggingWriter) SetUncompressedSize(origSize int64) {
200 w.UncompressedSize = origSize
203func (w *loggingWriter) WriteHeader(statusCode int) {
204 if w.StatusCode != 0 {
208 w.setStatusCode(statusCode)
210 // We transparently gzip-compress responses for requests under these conditions, all must apply:
212 // - Enabled for handler (static handlers make their own decisions).
213 // - Not a websocket request.
214 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
215 // - Not already compressed, or any other Content-Encoding header (including "identity").
216 // - Client accepts gzip encoded responses.
217 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
218 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")) {
219 // 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.
221 // We track the gzipped output for the access log.
222 cw := countWriter{Writer: w.W, Size: &w.Size}
223 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
224 w.W.Header().Set("Content-Encoding", "gzip")
225 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
227 w.W.WriteHeader(statusCode)
230func acceptsGzip(r *http.Request) bool {
231 s := r.Header.Get("Accept-Encoding")
232 t := strings.Split(s, ",")
233 for _, e := range t {
234 e = strings.TrimSpace(e)
235 tt := strings.Split(e, ";")
236 if len(tt) > 1 && t[1] == "q=0" {
246var compressibleTypes = map[string]bool{
247 "application/csv": true,
248 "application/javascript": true,
249 "application/json": true,
250 "application/x-javascript": true,
251 "application/xml": true,
252 "image/vnd.microsoft.icon": true,
253 "image/x-icon": true,
257 "font/opentype": true,
260func compressibleContentType(ct string) bool {
261 ct = strings.SplitN(ct, ";", 2)[0]
262 ct = strings.TrimSpace(ct)
263 ct = strings.ToLower(ct)
264 if compressibleTypes[ct] {
267 t, st, _ := strings.Cut(ct, "/")
268 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
271func compressibleContent(f *os.File) bool {
272 // We don't want to store many small files. They take up too much disk overhead.
273 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
277 buf := make([]byte, 512)
278 n, err := f.ReadAt(buf, 0)
279 if err != nil && err != io.EOF {
282 ct := http.DetectContentType(buf[:n])
283 return compressibleContentType(ct)
286type countWriter struct {
291func (w countWriter) Write(buf []byte) (int, error) {
292 n, err := w.Writer.Write(buf)
299var tlsVersions = map[uint16]string{
300 tls.VersionTLS10: "tls1.0",
301 tls.VersionTLS11: "tls1.1",
302 tls.VersionTLS12: "tls1.2",
303 tls.VersionTLS13: "tls1.3",
306func metricHTTPMethod(method string) string {
307 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
308 method = strings.ToLower(method)
310 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":
316func (w *loggingWriter) error(err error) {
322func (w *loggingWriter) Done() {
323 if w.Err == nil && w.Gzip != nil {
324 if err := w.Gzip.Close(); err != nil {
329 method := metricHTTPMethod(w.R.Method)
330 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
334 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
342 err = w.R.Context().Err()
344 attrs := []slog.Attr{
345 slog.String("httpaccess", ""),
346 slog.String("handler", w.Handler),
347 slog.String("method", method),
348 slog.Any("url", w.R.URL),
349 slog.String("host", w.R.Host),
350 slog.Duration("duration", time.Since(w.Start)),
351 slog.Int("statuscode", w.StatusCode),
352 slog.String("proto", strings.ToLower(w.R.Proto)),
353 slog.String("remoteaddr", w.R.RemoteAddr),
354 slog.String("tlsinfo", tlsinfo),
355 slog.String("useragent", w.R.Header.Get("User-Agent")),
356 slog.String("referer", w.R.Header.Get("Referer")),
359 s := w.R.Header.Get("X-Forwarded-For")
360 ipstr := strings.TrimSpace(strings.Split(s, ",")[0])
361 attrs = append(attrs,
362 slog.String("clientip", ipstr),
366 if w.WebsocketRequest {
367 attrs = append(attrs,
368 slog.Bool("websocketrequest", true),
371 if w.WebsocketResponse {
372 attrs = append(attrs,
373 slog.Bool("websocket", true),
374 slog.Int64("sizetoclient", w.SizeToClient),
375 slog.Int64("sizefromclient", w.SizeFromClient),
377 } else if w.UncompressedSize > 0 {
378 attrs = append(attrs,
379 slog.Int64("size", w.Size),
380 slog.Int64("uncompressedsize", w.UncompressedSize),
383 attrs = append(attrs,
384 slog.Int64("size", w.Size),
387 attrs = append(attrs, w.Attrs...)
388 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
391// Built-in handlers, e.g. mta-sts and autoconfig.
392type pathHandler struct {
393 Name string // For logging/metrics.
394 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.
395 Path string // Path to register, like on http.ServeMux.
400 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https).
401 TLSConfig *tls.Config
402 NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port.
404 Forwarded bool // Requests are coming from a reverse proxy, we'll use X-Forwarded-For for the IP address to ratelimit.
405 RateLimitDisabled bool // Don't apply ratelimiting.
407 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
408 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
409 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
410 // interfaces) last. WebHandlers can also pass requests to the internal servers.
411 // This order allows admins to serve other content on domains serving the mox.conf
412 // internal services.
413 SystemHandlers []pathHandler // Sorted, longest first.
415 ServiceHandlers []pathHandler // Sorted, longest first.
418// SystemHandle registers a named system handler for a path and optional host. If
419// path ends with a slash, it is used as prefix match, otherwise a full path match
420// is required. If hostOpt is set, only requests to those host are handled by this
422func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
423 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
426// Like SystemHandle, but for internal services "admin", "account", "webmail",
427// "webapi" configured in the mox.conf Listener.
428func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
429 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
433 limiterConnectionrate = &ratelimit.Limiter{
434 WindowLimits: []ratelimit.WindowLimit{
437 Limits: [...]int64{1000, 3000, 9000},
441 Limits: [...]int64{5000, 15000, 45000},
447// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
448// right pathHandler or WebHandler, and it generates access logs and tracks
450func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
453 // Rate limiting as early as possible, if enabled.
454 if !s.RateLimitDisabled {
455 // If requests are coming from a reverse proxy, use the IP from X-Forwarded-For.
456 // Otherwise the remote IP for this connection.
459 s := r.Header.Get("X-Forwarded-For")
460 ipstr = strings.TrimSpace(strings.Split(s, ",")[0])
462 pkglog.Debug("ratelimit: no ip address in X-Forwarded-For header")
466 ipstr, _, err = net.SplitHostPort(r.RemoteAddr)
468 pkglog.Debugx("ratelimit: parsing remote address", err, slog.String("remoteaddr", r.RemoteAddr))
471 ip := net.ParseIP(ipstr)
472 if ip == nil && ipstr != "" {
473 pkglog.Debug("ratelimit: invalid ip", slog.String("ip", ipstr))
475 if ip != nil && !limiterConnectionrate.Add(ip, now, 1) {
476 method := metricHTTPMethod(r.Method)
481 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
482 // No logging, that's just noise.
484 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
489 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
490 r = r.WithContext(ctx)
492 wf, ok := xw.(responseWriterFlusher)
494 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
498 nw := &loggingWriter{
502 Forwarded: s.Forwarded,
506 // Cleanup path, removing ".." and ".". Keep any trailing slash.
507 trailingPath := strings.HasSuffix(r.URL.Path, "/")
508 if r.URL.Path == "" {
511 r.URL.Path = path.Clean(r.URL.Path)
512 if r.URL.Path == "." {
515 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
520 nhost, _, err := net.SplitHostPort(host)
524 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
526 dom, domErr := dns.ParseDomain(host)
528 ipdom = dns.IPDomain{Domain: dom}
532 handle := func(h pathHandler) bool {
533 if h.HostMatch != nil && !h.HostMatch(ipdom) {
536 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
539 h.Handler.ServeHTTP(nw, r)
545 for _, h := range s.SystemHandlers {
551 if WebHandle(nw, r, ipdom) {
555 for _, h := range s.ServiceHandlers {
560 nw.Handler = "(nomatch)"
564func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
565 // Helpfully redirect user to version with ending slash.
566 if path != "/" && strings.HasSuffix(path, "/") {
567 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
568 http.Redirect(w, r, path, http.StatusSeeOther)
570 srv.ServiceHandle(name, hostMatch, strings.TrimRight(path, "/"), handler)
574// Listen binds to sockets for HTTP listeners, including those required for ACME to
575// generate TLS certificates. It stores the listeners so Serve can start serving them.
577 // Initialize listeners in deterministic order for the same potential error
579 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
580 for _, name := range names {
581 l := mox.Conf.Static.Listeners[name]
582 portServe := portServes(name, l)
584 ports := slices.Sorted(maps.Keys(portServe))
585 for _, port := range ports {
586 srv := portServe[port]
587 for _, ip := range l.IPs {
588 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, srv.NextProto)
594func portServes(name string, l config.Listener) map[int]*serve {
595 portServe := map[int]*serve{}
597 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
598 localhost := dns.Domain{ASCII: "localhost"}
600 ldom := l.HostnameDomain
601 if l.Hostname == "" {
602 ldom = mox.Conf.Static.HostnameDomain
604 listenerHostMatch := func(host dns.IPDomain) bool {
608 return host.Domain == ldom || host.Domain == localhost
610 accountHostMatch := func(host dns.IPDomain) bool {
611 if listenerHostMatch(host) {
614 return mox.Conf.IsClientSettingsDomain(host.Domain)
617 var ensureServe func(https, forwarded, noRateLimiting bool, port int, kind string, favicon bool) *serve
618 ensureServe = func(https, forwarded, rateLimitDisabled bool, port int, kind string, favicon bool) *serve {
621 s = &serve{nil, nil, tlsNextProtoMap{}, false, false, false, nil, false, nil}
624 s.Kinds = append(s.Kinds, kind)
625 if favicon && !s.Favicon {
626 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
629 s.Forwarded = s.Forwarded || forwarded
630 s.RateLimitDisabled = s.RateLimitDisabled || rateLimitDisabled
632 // We clone TLS configs because we may modify it later on for this server, for
633 // ALPN. And we need copies because multiple listeners on http.Server where the
634 // config is used will try to modify it concurrently.
635 if https && l.TLS.ACME != "" {
636 s.TLSConfig = l.TLS.ACMEConfig.Clone()
638 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
639 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
640 ensureServe(true, false, false, tlsport, "acme-tls-alpn-01", false)
643 s.TLSConfig = l.TLS.Config.Clone()
648 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
649 // yet, add http-01 validation mechanism handler to server.
650 ensureACMEHTTP01 := func(srv *serve) {
651 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
652 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
653 srv.Kinds = append(srv.Kinds, "acme-http-01")
654 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
658 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
659 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
660 ensureServe(true, false, false, port, "acme-tls-alpn-01", false)
662 if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
663 s := ensureServe(true, false, false, 443, "smtp-https", false)
664 hostname := mox.Conf.Static.HostnameDomain
665 if l.Hostname != "" {
666 hostname = l.HostnameDomain
669 maxMsgSize := l.SMTPMaxMessageSize
671 maxMsgSize = config.DefaultMaxMsgSize
673 requireTLS := !l.SMTP.NoRequireTLS
675 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
676 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
679 if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
680 s := ensureServe(true, false, false, 443, "imap-https", false)
681 s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
682 imapserver.ServeTLSConn(name, conn, s.TLSConfig)
685 if l.AccountHTTP.Enabled {
686 port := config.Port(l.AccountHTTP.Port, 80)
688 if l.AccountHTTP.Path != "" {
689 path = l.AccountHTTP.Path
691 srv := ensureServe(false, l.AccountHTTP.Forwarded, false, port, "account-http at "+path, true)
692 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
693 srv.ServiceHandle("account", accountHostMatch, path, handler)
694 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
695 ensureACMEHTTP01(srv)
697 if l.AccountHTTPS.Enabled {
698 port := config.Port(l.AccountHTTPS.Port, 443)
700 if l.AccountHTTPS.Path != "" {
701 path = l.AccountHTTPS.Path
703 srv := ensureServe(true, l.AccountHTTPS.Forwarded, false, port, "account-https at "+path, true)
704 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
705 srv.ServiceHandle("account", accountHostMatch, path, handler)
706 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
709 if l.AdminHTTP.Enabled {
710 port := config.Port(l.AdminHTTP.Port, 80)
712 if l.AdminHTTP.Path != "" {
713 path = l.AdminHTTP.Path
715 srv := ensureServe(false, l.AdminHTTP.Forwarded, false, port, "admin-http at "+path, true)
716 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
717 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
718 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
719 ensureACMEHTTP01(srv)
721 if l.AdminHTTPS.Enabled {
722 port := config.Port(l.AdminHTTPS.Port, 443)
724 if l.AdminHTTPS.Path != "" {
725 path = l.AdminHTTPS.Path
727 srv := ensureServe(true, l.AdminHTTPS.Forwarded, false, port, "admin-https at "+path, true)
728 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
729 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
730 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
733 maxMsgSize := l.SMTPMaxMessageSize
735 maxMsgSize = config.DefaultMaxMsgSize
738 if l.WebAPIHTTP.Enabled {
739 port := config.Port(l.WebAPIHTTP.Port, 80)
741 if l.WebAPIHTTP.Path != "" {
742 path = l.WebAPIHTTP.Path
744 srv := ensureServe(false, l.WebAPIHTTP.Forwarded, false, port, "webapi-http at "+path, true)
745 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
746 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
747 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
748 ensureACMEHTTP01(srv)
750 if l.WebAPIHTTPS.Enabled {
751 port := config.Port(l.WebAPIHTTPS.Port, 443)
753 if l.WebAPIHTTPS.Path != "" {
754 path = l.WebAPIHTTPS.Path
756 srv := ensureServe(true, l.WebAPIHTTPS.Forwarded, false, port, "webapi-https at "+path, true)
757 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
758 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
759 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
762 if l.WebmailHTTP.Enabled {
763 port := config.Port(l.WebmailHTTP.Port, 80)
765 if l.WebmailHTTP.Path != "" {
766 path = l.WebmailHTTP.Path
768 srv := ensureServe(false, l.WebmailHTTP.Forwarded, false, port, "webmail-http at "+path, true)
769 var accountPath string
770 if l.AccountHTTP.Enabled {
772 if l.AccountHTTP.Path != "" {
773 accountPath = l.AccountHTTP.Path
776 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
777 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
778 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
779 ensureACMEHTTP01(srv)
781 if l.WebmailHTTPS.Enabled {
782 port := config.Port(l.WebmailHTTPS.Port, 443)
784 if l.WebmailHTTPS.Path != "" {
785 path = l.WebmailHTTPS.Path
787 srv := ensureServe(true, l.WebmailHTTPS.Forwarded, false, port, "webmail-https at "+path, true)
788 var accountPath string
789 if l.AccountHTTPS.Enabled {
791 if l.AccountHTTPS.Path != "" {
792 accountPath = l.AccountHTTPS.Path
795 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
796 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
797 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
800 if l.MetricsHTTP.Enabled {
801 port := config.Port(l.MetricsHTTP.Port, 8010)
802 srv := ensureServe(false, false, false, port, "metrics-http", false)
803 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
804 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
805 if r.URL.Path != "/" {
808 } else if r.Method != "GET" {
809 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
812 w.Header().Set("Content-Type", "text/html")
813 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
816 if l.AutoconfigHTTPS.Enabled {
817 port := config.Port(l.AutoconfigHTTPS.Port, 443)
818 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, l.AutoconfigHTTPS.Forwarded, false, port, "autoconfig-https", false)
819 if l.AutoconfigHTTPS.NonTLS {
820 ensureACMEHTTP01(srv)
822 autoconfigMatch := func(ipdom dns.IPDomain) bool {
827 // Thunderbird requests an autodiscovery URL at the email address domain name, so
828 // autoconfig prefix is optional.
829 if after, ok := strings.CutPrefix(dom.ASCII, "autoconfig."); ok {
831 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
833 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
834 // use the mail server's host name.
835 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
838 dc, ok := mox.Conf.Domain(dom)
839 return ok && !dc.ReportsOnly
841 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
842 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
843 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
844 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
846 if l.MTASTSHTTPS.Enabled {
847 port := config.Port(l.MTASTSHTTPS.Port, 443)
848 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, l.MTASTSHTTPS.Forwarded, false, port, "mtasts-https", false)
849 if l.MTASTSHTTPS.NonTLS {
850 ensureACMEHTTP01(srv)
852 mtastsMatch := func(ipdom dns.IPDomain) bool {
853 // todo: may want to check this against the configured domains, could in theory be just a webserver.
858 return strings.HasPrefix(dom.ASCII, "mta-sts.")
860 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
862 if l.PprofHTTP.Enabled {
863 // Importing net/http/pprof registers handlers on the default serve mux.
864 port := config.Port(l.PprofHTTP.Port, 8011)
865 if _, ok := portServe[port]; ok {
866 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
868 srv := &serve{[]string{"pprof-http"}, nil, nil, false, false, false, nil, false, nil}
869 portServe[port] = srv
870 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
872 if l.WebserverHTTP.Enabled {
873 port := config.Port(l.WebserverHTTP.Port, 80)
874 srv := ensureServe(false, false, l.WebserverHTTP.RateLimitDisabled, port, "webserver-http", false)
876 ensureACMEHTTP01(srv)
878 if l.WebserverHTTPS.Enabled {
879 port := config.Port(l.WebserverHTTPS.Port, 443)
880 srv := ensureServe(true, false, l.WebserverHTTPS.RateLimitDisabled, port, "webserver-https", false)
884 if l.TLS != nil && l.TLS.ACME != "" {
885 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
886 if ensureManagerHosts[m] == nil {
887 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
889 hosts := ensureManagerHosts[m]
890 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
892 if l.HostnameDomain.ASCII != "" {
893 hosts[l.HostnameDomain] = struct{}{}
896 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
897 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
898 // for the timeouts of clients doing autoconfig.
900 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
901 for _, name := range mox.Conf.Domains() {
902 if dom, err := dns.ParseDomain(name); err != nil {
903 pkglog.Errorx("parsing domain from config", err)
904 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly || d.Disabled {
905 // Do not gather autoconfig name if we aren't accepting email for this domain or when it is disabled.
909 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
911 pkglog.Errorx("parsing domain from config for autoconfig", err)
913 hosts[autoconfdom] = struct{}{}
919 if s := portServe[443]; s != nil && s.TLSConfig != nil && len(s.NextProto) > 0 {
920 s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, slices.Collect(maps.Keys(s.NextProto))...)
923 for _, srv := range portServe {
924 sortPathHandlers(srv.SystemHandlers)
925 sortPathHandlers(srv.ServiceHandlers)
931func sortPathHandlers(l []pathHandler) {
932 sort.Slice(l, func(i, j int) bool {
935 if len(a) == len(b) {
936 // For consistent order.
939 // Longest paths first.
940 return len(a) > len(b)
944// functions to be launched in goroutine that will serve on a listener.
947// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
948// immediately after startup. We only do so for our explicit listener hostnames,
949// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
950// do request autoconfig, otherwise clients may run into their timeouts waiting for
951// the certificate to be given during the first https connection.
952var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
954type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
956// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
957func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, nextProto tlsNextProtoMap) {
958 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
963 if tlsConfig == nil {
965 if os.Getuid() == 0 {
966 pkglog.Print("http listener",
967 slog.String("name", name),
968 slog.String("kinds", strings.Join(kinds, ",")),
969 slog.String("address", addr))
971 ln, err = mox.Listen(mox.Network(ip), addr)
973 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
977 if os.Getuid() == 0 {
978 pkglog.Print("https listener",
979 slog.String("name", name),
980 slog.String("kinds", strings.Join(kinds, ",")),
981 slog.String("address", addr))
983 ln, err = mox.Listen(mox.Network(ip), addr)
985 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
987 ln = tls.NewListener(ln, tlsConfig)
990 server := &http.Server{
992 TLSConfig: tlsConfig,
993 ReadHeaderTimeout: 30 * time.Second,
994 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
995 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
996 TLSNextProto: nextProto,
998 // By default, the Go 1.6 and above http.Server includes support for HTTP2.
999 // However, HTTP2 is negotiated via ALPN. Because we are configuring
1000 // TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
1001 // and calling ConfigureServer.
1002 err = http2.ConfigureServer(server, nil)
1004 pkglog.Fatalx("https: unable to configure http2", err)
1007 err := server.Serve(ln)
1008 pkglog.Fatalx(protocol+": serve", err)
1010 servers = append(servers, serve)
1013// Serve starts serving on the initialized listeners.
1015 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
1017 go webaccount.ImportManage()
1019 for _, serve := range servers {
1025 time.Sleep(1 * time.Second)
1027 for m, hosts := range ensureManagerHosts {
1028 for host := range hosts {
1029 // Check if certificate is already available. If so, we don't print as much after a
1030 // restart, and finish more quickly if only a few certificates are missing/old.
1031 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
1032 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
1038 // Just in case someone adds quite some domains to their config. We don't want to
1039 // hit any ACME rate limits.
1043 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
1044 time.Sleep(10 * time.Second)
1048 hello := &tls.ClientHelloInfo{
1049 ServerName: host.ASCII,
1051 // Make us fetch an ECDSA P256 cert.
1052 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
1053 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
1054 SupportedCurves: []tls.CurveID{tls.CurveP256},
1055 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
1056 SupportedVersions: []uint16{tls.VersionTLS13},
1058 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
1059 if _, err := m.Manager.GetCertificate(hello); err != nil {
1060 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))