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.
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("referer", w.R.Header.Get("Referer")),
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.
391 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https).
392 TLSConfig *tls.Config
393 NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port.
395 Forwarded bool // Requests are coming from a reverse proxy, we'll use X-Forwarded-For for the IP address to ratelimit.
396 RateLimitDisabled bool // Don't apply ratelimiting.
398 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
399 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
400 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
401 // interfaces) last. WebHandlers can also pass requests to the internal servers.
402 // This order allows admins to serve other content on domains serving the mox.conf
403 // internal services.
404 SystemHandlers []pathHandler // Sorted, longest first.
406 ServiceHandlers []pathHandler // Sorted, longest first.
409// SystemHandle registers a named system handler for a path and optional host. If
410// path ends with a slash, it is used as prefix match, otherwise a full path match
411// is required. If hostOpt is set, only requests to those host are handled by this
413func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
414 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
417// Like SystemHandle, but for internal services "admin", "account", "webmail",
418// "webapi" configured in the mox.conf Listener.
419func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
420 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
424 limiterConnectionrate = &ratelimit.Limiter{
425 WindowLimits: []ratelimit.WindowLimit{
428 Limits: [...]int64{1000, 3000, 9000},
432 Limits: [...]int64{5000, 15000, 45000},
438// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
439// right pathHandler or WebHandler, and it generates access logs and tracks
441func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
444 // Rate limiting as early as possible, if enabled.
445 if !s.RateLimitDisabled {
446 // If requests are coming from a reverse proxy, use the IP from X-Forwarded-For.
447 // Otherwise the remote IP for this connection.
450 s := r.Header.Get("X-Forwarded-For")
451 ipstr = strings.TrimSpace(strings.Split(s, ",")[0])
453 pkglog.Debug("ratelimit: no ip address in X-Forwarded-For header")
457 ipstr, _, err = net.SplitHostPort(r.RemoteAddr)
459 pkglog.Debugx("ratelimit: parsing remote address", err, slog.String("remoteaddr", r.RemoteAddr))
462 ip := net.ParseIP(ipstr)
463 if ip == nil && ipstr != "" {
464 pkglog.Debug("ratelimit: invalid ip", slog.String("ip", ipstr))
466 if ip != nil && !limiterConnectionrate.Add(ip, now, 1) {
467 method := metricHTTPMethod(r.Method)
472 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
473 // No logging, that's just noise.
475 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
480 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
481 r = r.WithContext(ctx)
483 wf, ok := xw.(responseWriterFlusher)
485 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
489 nw := &loggingWriter{
496 // Cleanup path, removing ".." and ".". Keep any trailing slash.
497 trailingPath := strings.HasSuffix(r.URL.Path, "/")
498 if r.URL.Path == "" {
501 r.URL.Path = path.Clean(r.URL.Path)
502 if r.URL.Path == "." {
505 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
510 nhost, _, err := net.SplitHostPort(host)
514 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
516 dom, domErr := dns.ParseDomain(host)
518 ipdom = dns.IPDomain{Domain: dom}
522 handle := func(h pathHandler) bool {
523 if h.HostMatch != nil && !h.HostMatch(ipdom) {
526 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
529 h.Handler.ServeHTTP(nw, r)
535 for _, h := range s.SystemHandlers {
541 if WebHandle(nw, r, ipdom) {
545 for _, h := range s.ServiceHandlers {
550 nw.Handler = "(nomatch)"
554func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
555 // Helpfully redirect user to version with ending slash.
556 if path != "/" && strings.HasSuffix(path, "/") {
557 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
558 http.Redirect(w, r, path, http.StatusSeeOther)
560 srv.ServiceHandle(name, hostMatch, strings.TrimRight(path, "/"), handler)
564// Listen binds to sockets for HTTP listeners, including those required for ACME to
565// generate TLS certificates. It stores the listeners so Serve can start serving them.
567 // Initialize listeners in deterministic order for the same potential error
569 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
570 for _, name := range names {
571 l := mox.Conf.Static.Listeners[name]
572 portServe := portServes(name, l)
574 ports := slices.Sorted(maps.Keys(portServe))
575 for _, port := range ports {
576 srv := portServe[port]
577 for _, ip := range l.IPs {
578 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, srv.NextProto)
584func portServes(name string, l config.Listener) map[int]*serve {
585 portServe := map[int]*serve{}
587 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
588 localhost := dns.Domain{ASCII: "localhost"}
590 ldom := l.HostnameDomain
591 if l.Hostname == "" {
592 ldom = mox.Conf.Static.HostnameDomain
594 listenerHostMatch := func(host dns.IPDomain) bool {
598 return host.Domain == ldom || host.Domain == localhost
600 accountHostMatch := func(host dns.IPDomain) bool {
601 if listenerHostMatch(host) {
604 return mox.Conf.IsClientSettingsDomain(host.Domain)
607 var ensureServe func(https, forwarded, noRateLimiting bool, port int, kind string, favicon bool) *serve
608 ensureServe = func(https, forwarded, rateLimitDisabled bool, port int, kind string, favicon bool) *serve {
611 s = &serve{nil, nil, tlsNextProtoMap{}, false, false, false, nil, false, nil}
614 s.Kinds = append(s.Kinds, kind)
615 if favicon && !s.Favicon {
616 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
619 s.Forwarded = s.Forwarded || forwarded
620 s.RateLimitDisabled = s.RateLimitDisabled || rateLimitDisabled
622 // We clone TLS configs because we may modify it later on for this server, for
623 // ALPN. And we need copies because multiple listeners on http.Server where the
624 // config is used will try to modify it concurrently.
625 if https && l.TLS.ACME != "" {
626 s.TLSConfig = l.TLS.ACMEConfig.Clone()
628 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
629 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
630 ensureServe(true, false, false, tlsport, "acme-tls-alpn-01", false)
633 s.TLSConfig = l.TLS.Config.Clone()
638 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
639 // yet, add http-01 validation mechanism handler to server.
640 ensureACMEHTTP01 := func(srv *serve) {
641 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
642 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
643 srv.Kinds = append(srv.Kinds, "acme-http-01")
644 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
648 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
649 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
650 ensureServe(true, false, false, port, "acme-tls-alpn-01", false)
652 if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
653 s := ensureServe(true, false, false, 443, "smtp-https", false)
654 hostname := mox.Conf.Static.HostnameDomain
655 if l.Hostname != "" {
656 hostname = l.HostnameDomain
659 maxMsgSize := l.SMTPMaxMessageSize
661 maxMsgSize = config.DefaultMaxMsgSize
663 requireTLS := !l.SMTP.NoRequireTLS
665 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
666 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
669 if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
670 s := ensureServe(true, false, false, 443, "imap-https", false)
671 s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
672 imapserver.ServeTLSConn(name, conn, s.TLSConfig)
675 if l.AccountHTTP.Enabled {
676 port := config.Port(l.AccountHTTP.Port, 80)
678 if l.AccountHTTP.Path != "" {
679 path = l.AccountHTTP.Path
681 srv := ensureServe(false, l.AccountHTTP.Forwarded, false, port, "account-http at "+path, true)
682 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
683 srv.ServiceHandle("account", accountHostMatch, path, handler)
684 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
685 ensureACMEHTTP01(srv)
687 if l.AccountHTTPS.Enabled {
688 port := config.Port(l.AccountHTTPS.Port, 443)
690 if l.AccountHTTPS.Path != "" {
691 path = l.AccountHTTPS.Path
693 srv := ensureServe(true, l.AccountHTTPS.Forwarded, false, port, "account-https at "+path, true)
694 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
695 srv.ServiceHandle("account", accountHostMatch, path, handler)
696 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
699 if l.AdminHTTP.Enabled {
700 port := config.Port(l.AdminHTTP.Port, 80)
702 if l.AdminHTTP.Path != "" {
703 path = l.AdminHTTP.Path
705 srv := ensureServe(false, l.AdminHTTP.Forwarded, false, port, "admin-http at "+path, true)
706 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
707 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
708 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
709 ensureACMEHTTP01(srv)
711 if l.AdminHTTPS.Enabled {
712 port := config.Port(l.AdminHTTPS.Port, 443)
714 if l.AdminHTTPS.Path != "" {
715 path = l.AdminHTTPS.Path
717 srv := ensureServe(true, l.AdminHTTPS.Forwarded, false, port, "admin-https at "+path, true)
718 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
719 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
720 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
723 maxMsgSize := l.SMTPMaxMessageSize
725 maxMsgSize = config.DefaultMaxMsgSize
728 if l.WebAPIHTTP.Enabled {
729 port := config.Port(l.WebAPIHTTP.Port, 80)
731 if l.WebAPIHTTP.Path != "" {
732 path = l.WebAPIHTTP.Path
734 srv := ensureServe(false, l.WebAPIHTTP.Forwarded, false, port, "webapi-http at "+path, true)
735 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
736 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
737 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
738 ensureACMEHTTP01(srv)
740 if l.WebAPIHTTPS.Enabled {
741 port := config.Port(l.WebAPIHTTPS.Port, 443)
743 if l.WebAPIHTTPS.Path != "" {
744 path = l.WebAPIHTTPS.Path
746 srv := ensureServe(true, l.WebAPIHTTPS.Forwarded, false, port, "webapi-https at "+path, true)
747 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
748 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
749 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
752 if l.WebmailHTTP.Enabled {
753 port := config.Port(l.WebmailHTTP.Port, 80)
755 if l.WebmailHTTP.Path != "" {
756 path = l.WebmailHTTP.Path
758 srv := ensureServe(false, l.WebmailHTTP.Forwarded, false, port, "webmail-http at "+path, true)
759 var accountPath string
760 if l.AccountHTTP.Enabled {
762 if l.AccountHTTP.Path != "" {
763 accountPath = l.AccountHTTP.Path
766 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
767 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
768 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
769 ensureACMEHTTP01(srv)
771 if l.WebmailHTTPS.Enabled {
772 port := config.Port(l.WebmailHTTPS.Port, 443)
774 if l.WebmailHTTPS.Path != "" {
775 path = l.WebmailHTTPS.Path
777 srv := ensureServe(true, l.WebmailHTTPS.Forwarded, false, port, "webmail-https at "+path, true)
778 var accountPath string
779 if l.AccountHTTPS.Enabled {
781 if l.AccountHTTPS.Path != "" {
782 accountPath = l.AccountHTTPS.Path
785 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
786 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
787 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
790 if l.MetricsHTTP.Enabled {
791 port := config.Port(l.MetricsHTTP.Port, 8010)
792 srv := ensureServe(false, false, false, port, "metrics-http", false)
793 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
794 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
795 if r.URL.Path != "/" {
798 } else if r.Method != "GET" {
799 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
802 w.Header().Set("Content-Type", "text/html")
803 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
806 if l.AutoconfigHTTPS.Enabled {
807 port := config.Port(l.AutoconfigHTTPS.Port, 443)
808 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, false, false, port, "autoconfig-https", false)
809 if l.AutoconfigHTTPS.NonTLS {
810 ensureACMEHTTP01(srv)
812 autoconfigMatch := func(ipdom dns.IPDomain) bool {
817 // Thunderbird requests an autodiscovery URL at the email address domain name, so
818 // autoconfig prefix is optional.
819 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
820 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
821 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
823 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
824 // use the mail server's host name.
825 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
828 dc, ok := mox.Conf.Domain(dom)
829 return ok && !dc.ReportsOnly
831 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
832 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
833 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
834 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
836 if l.MTASTSHTTPS.Enabled {
837 port := config.Port(l.MTASTSHTTPS.Port, 443)
838 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, false, false, port, "mtasts-https", false)
839 if l.MTASTSHTTPS.NonTLS {
840 ensureACMEHTTP01(srv)
842 mtastsMatch := func(ipdom dns.IPDomain) bool {
843 // todo: may want to check this against the configured domains, could in theory be just a webserver.
848 return strings.HasPrefix(dom.ASCII, "mta-sts.")
850 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
852 if l.PprofHTTP.Enabled {
853 // Importing net/http/pprof registers handlers on the default serve mux.
854 port := config.Port(l.PprofHTTP.Port, 8011)
855 if _, ok := portServe[port]; ok {
856 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
858 srv := &serve{[]string{"pprof-http"}, nil, nil, false, false, false, nil, false, nil}
859 portServe[port] = srv
860 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
862 if l.WebserverHTTP.Enabled {
863 port := config.Port(l.WebserverHTTP.Port, 80)
864 srv := ensureServe(false, false, l.WebserverHTTP.RateLimitDisabled, port, "webserver-http", false)
866 ensureACMEHTTP01(srv)
868 if l.WebserverHTTPS.Enabled {
869 port := config.Port(l.WebserverHTTPS.Port, 443)
870 srv := ensureServe(true, false, l.WebserverHTTPS.RateLimitDisabled, port, "webserver-https", false)
874 if l.TLS != nil && l.TLS.ACME != "" {
875 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
876 if ensureManagerHosts[m] == nil {
877 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
879 hosts := ensureManagerHosts[m]
880 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
882 if l.HostnameDomain.ASCII != "" {
883 hosts[l.HostnameDomain] = struct{}{}
886 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
887 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
888 // for the timeouts of clients doing autoconfig.
890 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
891 for _, name := range mox.Conf.Domains() {
892 if dom, err := dns.ParseDomain(name); err != nil {
893 pkglog.Errorx("parsing domain from config", err)
894 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly || d.Disabled {
895 // Do not gather autoconfig name if we aren't accepting email for this domain or when it is disabled.
899 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
901 pkglog.Errorx("parsing domain from config for autoconfig", err)
903 hosts[autoconfdom] = struct{}{}
909 if s := portServe[443]; s != nil && s.TLSConfig != nil && len(s.NextProto) > 0 {
910 s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, slices.Collect(maps.Keys(s.NextProto))...)
913 for _, srv := range portServe {
914 sortPathHandlers(srv.SystemHandlers)
915 sortPathHandlers(srv.ServiceHandlers)
921func sortPathHandlers(l []pathHandler) {
922 sort.Slice(l, func(i, j int) bool {
925 if len(a) == len(b) {
926 // For consistent order.
929 // Longest paths first.
930 return len(a) > len(b)
934// functions to be launched in goroutine that will serve on a listener.
937// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
938// immediately after startup. We only do so for our explicit listener hostnames,
939// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
940// do request autoconfig, otherwise clients may run into their timeouts waiting for
941// the certificate to be given during the first https connection.
942var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
944type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
946// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
947func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, nextProto tlsNextProtoMap) {
948 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
953 if tlsConfig == nil {
955 if os.Getuid() == 0 {
956 pkglog.Print("http listener",
957 slog.String("name", name),
958 slog.String("kinds", strings.Join(kinds, ",")),
959 slog.String("address", addr))
961 ln, err = mox.Listen(mox.Network(ip), addr)
963 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
967 if os.Getuid() == 0 {
968 pkglog.Print("https listener",
969 slog.String("name", name),
970 slog.String("kinds", strings.Join(kinds, ",")),
971 slog.String("address", addr))
973 ln, err = mox.Listen(mox.Network(ip), addr)
975 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
977 ln = tls.NewListener(ln, tlsConfig)
980 server := &http.Server{
982 TLSConfig: tlsConfig,
983 ReadHeaderTimeout: 30 * time.Second,
984 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
985 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
986 TLSNextProto: nextProto,
988 // By default, the Go 1.6 and above http.Server includes support for HTTP2.
989 // However, HTTP2 is negotiated via ALPN. Because we are configuring
990 // TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
991 // and calling ConfigureServer.
992 err = http2.ConfigureServer(server, nil)
994 pkglog.Fatalx("https: unable to configure http2", err)
997 err := server.Serve(ln)
998 pkglog.Fatalx(protocol+": serve", err)
1000 servers = append(servers, serve)
1003// Serve starts serving on the initialized listeners.
1005 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
1007 go webaccount.ImportManage()
1009 for _, serve := range servers {
1015 time.Sleep(1 * time.Second)
1017 for m, hosts := range ensureManagerHosts {
1018 for host := range hosts {
1019 // Check if certificate is already available. If so, we don't print as much after a
1020 // restart, and finish more quickly if only a few certificates are missing/old.
1021 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
1022 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
1028 // Just in case someone adds quite some domains to their config. We don't want to
1029 // hit any ACME rate limits.
1033 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
1034 time.Sleep(10 * time.Second)
1038 hello := &tls.ClientHelloInfo{
1039 ServerName: host.ASCII,
1041 // Make us fetch an ECDSA P256 cert.
1042 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
1043 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
1044 SupportedCurves: []tls.CurveID{tls.CurveP256},
1045 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
1046 SupportedVersions: []uint16{tls.VersionTLS13},
1048 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
1049 if _, err := m.Manager.GetCertificate(hello); err != nil {
1050 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))