1// Package http provides HTTP listeners/servers, for
2// autoconfiguration/autodiscovery, the account and admin web interface and
24 "golang.org/x/exp/maps"
26 "github.com/prometheus/client_golang/prometheus"
27 "github.com/prometheus/client_golang/prometheus/promauto"
28 "github.com/prometheus/client_golang/prometheus/promhttp"
30 "github.com/mjl-/mox/autotls"
31 "github.com/mjl-/mox/config"
32 "github.com/mjl-/mox/dns"
33 "github.com/mjl-/mox/mlog"
34 "github.com/mjl-/mox/mox-"
35 "github.com/mjl-/mox/ratelimit"
36 "github.com/mjl-/mox/webaccount"
37 "github.com/mjl-/mox/webadmin"
38 "github.com/mjl-/mox/webmail"
41var pkglog = mlog.New("http", nil)
44 // metricRequest tracks performance (time to write response header) of server.
45 metricRequest = promauto.NewHistogramVec(
46 prometheus.HistogramOpts{
47 Name: "mox_httpserver_request_duration_seconds",
48 Help: "HTTP(s) server request with handler name, protocol, method, result codes, and duration until response status code is written, in seconds.",
49 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
52 "handler", // Name from webhandler, can be empty.
53 "proto", // "http", "https", "ws", "wss"
54 "method", // "(unknown)" and otherwise only common verbs
58 // metricResponse tracks performance of entire request as experienced by users,
59 // which also depends on their connection speed, so not necessarily something you
61 metricResponse = promauto.NewHistogramVec(
62 prometheus.HistogramOpts{
63 Name: "mox_httpserver_response_duration_seconds",
64 Help: "HTTP(s) server response with handler name, protocol, method, result codes, and duration of entire response, in seconds.",
65 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
68 "handler", // Name from webhandler, can be empty.
69 "proto", // "http", "https", "ws", "wss"
70 "method", // "(unknown)" and otherwise only common verbs
76type responseWriterFlusher interface {
81// http.ResponseWriter that writes access log and tracks metrics at end of response.
82type loggingWriter struct {
83 W responseWriterFlusher // Calls are forwarded.
86 WebsocketRequest bool // Whether request from was websocket.
94 Size int64 // Of data served to client, for non-websocket responses.
95 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
96 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
98 WebsocketResponse bool // If this was a successful websocket connection with backend.
99 SizeFromClient, SizeToClient int64 // Websocket data.
100 Attrs []slog.Attr // Additional fields to log.
103func (w *loggingWriter) AddAttr(a slog.Attr) {
104 w.Attrs = append(w.Attrs, a)
107func (w *loggingWriter) Flush() {
111func (w *loggingWriter) Header() http.Header {
115// protocol, for logging.
116func (w *loggingWriter) proto(websocket bool) string {
127func (w *loggingWriter) Write(buf []byte) (int, error) {
128 if w.StatusCode == 0 {
129 w.WriteHeader(http.StatusOK)
135 n, err = w.W.Write(buf)
140 // We flush after each write. Probably takes a few more bytes, but prevents any
141 // issues due to buffering.
142 // w.Gzip.Write updates w.Size with the compressed byte count.
143 n, err = w.Gzip.Write(buf)
148 w.UncompressedSize += int64(n)
157func (w *loggingWriter) setStatusCode(statusCode int) {
158 if w.StatusCode != 0 {
162 w.StatusCode = statusCode
163 method := metricHTTPMethod(w.R.Method)
164 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
167// SetUncompressedSize is used through an interface by
168// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
169func (w *loggingWriter) SetUncompressedSize(origSize int64) {
170 w.UncompressedSize = origSize
173func (w *loggingWriter) WriteHeader(statusCode int) {
174 if w.StatusCode != 0 {
178 w.setStatusCode(statusCode)
180 // We transparently gzip-compress responses for requests under these conditions, all must apply:
182 // - Enabled for handler (static handlers make their own decisions).
183 // - Not a websocket request.
184 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
185 // - Not already compressed, or any other Content-Encoding header (including "identity").
186 // - Client accepts gzip encoded responses.
187 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
188 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")) {
189 // 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.
191 // We track the gzipped output for the access log.
192 cw := countWriter{Writer: w.W, Size: &w.Size}
193 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
194 w.W.Header().Set("Content-Encoding", "gzip")
195 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
197 w.W.WriteHeader(statusCode)
200func acceptsGzip(r *http.Request) bool {
201 s := r.Header.Get("Accept-Encoding")
202 t := strings.Split(s, ",")
203 for _, e := range t {
204 e = strings.TrimSpace(e)
205 tt := strings.Split(e, ";")
206 if len(tt) > 1 && t[1] == "q=0" {
216var compressibleTypes = map[string]bool{
217 "application/csv": true,
218 "application/javascript": true,
219 "application/json": true,
220 "application/x-javascript": true,
221 "application/xml": true,
222 "image/vnd.microsoft.icon": true,
223 "image/x-icon": true,
227 "font/opentype": true,
230func compressibleContentType(ct string) bool {
231 ct = strings.SplitN(ct, ";", 2)[0]
232 ct = strings.TrimSpace(ct)
233 ct = strings.ToLower(ct)
234 if compressibleTypes[ct] {
237 t, st, _ := strings.Cut(ct, "/")
238 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
241func compressibleContent(f *os.File) bool {
242 // We don't want to store many small files. They take up too much disk overhead.
243 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
247 buf := make([]byte, 512)
248 n, err := f.ReadAt(buf, 0)
249 if err != nil && err != io.EOF {
252 ct := http.DetectContentType(buf[:n])
253 return compressibleContentType(ct)
256type countWriter struct {
261func (w countWriter) Write(buf []byte) (int, error) {
262 n, err := w.Writer.Write(buf)
269var tlsVersions = map[uint16]string{
270 tls.VersionTLS10: "tls1.0",
271 tls.VersionTLS11: "tls1.1",
272 tls.VersionTLS12: "tls1.2",
273 tls.VersionTLS13: "tls1.3",
276func metricHTTPMethod(method string) string {
277 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
278 method = strings.ToLower(method)
280 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":
286func (w *loggingWriter) error(err error) {
292func (w *loggingWriter) Done() {
293 if w.Err == nil && w.Gzip != nil {
294 if err := w.Gzip.Close(); err != nil {
299 method := metricHTTPMethod(w.R.Method)
300 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
304 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
312 err = w.R.Context().Err()
314 attrs := []slog.Attr{
315 slog.String("httpaccess", ""),
316 slog.String("handler", w.Handler),
317 slog.String("method", method),
318 slog.Any("url", w.R.URL),
319 slog.String("host", w.R.Host),
320 slog.Duration("duration", time.Since(w.Start)),
321 slog.Int("statuscode", w.StatusCode),
322 slog.String("proto", strings.ToLower(w.R.Proto)),
323 slog.Any("remoteaddr", w.R.RemoteAddr),
324 slog.String("tlsinfo", tlsinfo),
325 slog.String("useragent", w.R.Header.Get("User-Agent")),
326 slog.String("referrr", w.R.Header.Get("Referrer")),
328 if w.WebsocketRequest {
329 attrs = append(attrs,
330 slog.Bool("websocketrequest", true),
333 if w.WebsocketResponse {
334 attrs = append(attrs,
335 slog.Bool("websocket", true),
336 slog.Int64("sizetoclient", w.SizeToClient),
337 slog.Int64("sizefromclient", w.SizeFromClient),
339 } else if w.UncompressedSize > 0 {
340 attrs = append(attrs,
341 slog.Int64("size", w.Size),
342 slog.Int64("uncompressedsize", w.UncompressedSize),
345 attrs = append(attrs,
346 slog.Int64("size", w.Size),
349 attrs = append(attrs, w.Attrs...)
350 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
353// Set some http headers that should prevent potential abuse. Better safe than sorry.
354func safeHeaders(fn http.Handler) http.Handler {
355 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
357 h.Set("X-Frame-Options", "deny")
358 h.Set("X-Content-Type-Options", "nosniff")
359 h.Set("Content-Security-Policy", "default-src 'self' 'unsafe-inline' data:")
360 h.Set("Referrer-Policy", "same-origin")
365// Built-in handlers, e.g. mta-sts and autoconfig.
366type pathHandler struct {
367 Name string // For logging/metrics.
368 HostMatch func(dom dns.Domain) bool // If not nil, called to see if domain of requests matches. Only called if requested host is a valid domain.
369 Path string // Path to register, like on http.ServeMux.
373 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https).
374 TLSConfig *tls.Config
375 PathHandlers []pathHandler // Sorted, longest first.
376 Webserver bool // Whether serving WebHandler. PathHandlers are always evaluated before WebHandlers.
379// Handle registers a named handler for a path and optional host. If path ends with
380// a slash, it is used as prefix match, otherwise a full path match is required. If
381// hostOpt is set, only requests to those host are handled by this handler.
382func (s *serve) Handle(name string, hostMatch func(dns.Domain) bool, path string, fn http.Handler) {
383 s.PathHandlers = append(s.PathHandlers, pathHandler{name, hostMatch, path, fn})
387 limiterConnectionrate = &ratelimit.Limiter{
388 WindowLimits: []ratelimit.WindowLimit{
391 Limits: [...]int64{1000, 3000, 9000},
395 Limits: [...]int64{5000, 15000, 45000},
401// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
402// right pathHandler or WebHandler, and it generates access logs and tracks
404func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
406 // Rate limiting as early as possible.
407 ipstr, _, err := net.SplitHostPort(r.RemoteAddr)
409 pkglog.Debugx("split host:port client remoteaddr", err, slog.Any("remoteaddr", r.RemoteAddr))
410 } else if ip := net.ParseIP(ipstr); ip == nil {
411 pkglog.Debug("parsing ip for client remoteaddr", slog.Any("remoteaddr", r.RemoteAddr))
412 } else if !limiterConnectionrate.Add(ip, now, 1) {
413 method := metricHTTPMethod(r.Method)
418 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
419 // No logging, that's just noise.
421 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
425 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
426 r = r.WithContext(ctx)
428 wf, ok := xw.(responseWriterFlusher)
430 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
434 nw := &loggingWriter{
441 // Cleanup path, removing ".." and ".". Keep any trailing slash.
442 trailingPath := strings.HasSuffix(r.URL.Path, "/")
443 if r.URL.Path == "" {
446 r.URL.Path = path.Clean(r.URL.Path)
447 if r.URL.Path == "." {
450 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
456 nhost, _, err := net.SplitHostPort(host)
460 // host could be an IP, some handles may match, not an error.
461 dom, domErr := dns.ParseDomain(host)
463 for _, h := range s.PathHandlers {
464 if h.HostMatch != nil && (domErr != nil || !h.HostMatch(dom)) {
467 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
470 h.Handler.ServeHTTP(nw, r)
474 if s.Webserver && domErr == nil {
475 if WebHandle(nw, r, dom) {
479 nw.Handler = "(nomatch)"
483// Listen binds to sockets for HTTP listeners, including those required for ACME to
484// generate TLS certificates. It stores the listeners so Serve can start serving them.
486 redirectToTrailingSlash := func(srv *serve, name, path string) {
487 // Helpfully redirect user to version with ending slash.
488 if path != "/" && strings.HasSuffix(path, "/") {
489 handler := safeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
490 http.Redirect(w, r, path, http.StatusSeeOther)
492 srv.Handle(name, nil, path[:len(path)-1], handler)
496 // Initialize listeners in deterministic order for the same potential error
498 names := maps.Keys(mox.Conf.Static.Listeners)
500 for _, name := range names {
501 l := mox.Conf.Static.Listeners[name]
503 portServe := map[int]*serve{}
505 var ensureServe func(https bool, port int, kind string) *serve
506 ensureServe = func(https bool, port int, kind string) *serve {
509 s = &serve{nil, nil, nil, false}
512 s.Kinds = append(s.Kinds, kind)
513 if https && l.TLS.ACME != "" {
514 s.TLSConfig = l.TLS.ACMEConfig
516 s.TLSConfig = l.TLS.Config
517 if l.TLS.ACME != "" {
518 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
519 ensureServe(true, tlsport, "acme-tls-alpn-01")
525 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
526 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
527 ensureServe(true, port, "acme-tls-alpn-01")
530 if l.AccountHTTP.Enabled {
531 port := config.Port(l.AccountHTTP.Port, 80)
533 if l.AccountHTTP.Path != "" {
534 path = l.AccountHTTP.Path
536 srv := ensureServe(false, port, "account-http at "+path)
537 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
538 srv.Handle("account", nil, path, handler)
539 redirectToTrailingSlash(srv, "account", path)
541 if l.AccountHTTPS.Enabled {
542 port := config.Port(l.AccountHTTPS.Port, 443)
544 if l.AccountHTTPS.Path != "" {
545 path = l.AccountHTTPS.Path
547 srv := ensureServe(true, port, "account-https at "+path)
548 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
549 srv.Handle("account", nil, path, handler)
550 redirectToTrailingSlash(srv, "account", path)
553 if l.AdminHTTP.Enabled {
554 port := config.Port(l.AdminHTTP.Port, 80)
556 if l.AdminHTTP.Path != "" {
557 path = l.AdminHTTP.Path
559 srv := ensureServe(false, port, "admin-http at "+path)
560 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
561 srv.Handle("admin", nil, path, handler)
562 redirectToTrailingSlash(srv, "admin", path)
564 if l.AdminHTTPS.Enabled {
565 port := config.Port(l.AdminHTTPS.Port, 443)
567 if l.AdminHTTPS.Path != "" {
568 path = l.AdminHTTPS.Path
570 srv := ensureServe(true, port, "admin-https at "+path)
571 handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
572 srv.Handle("admin", nil, path, handler)
573 redirectToTrailingSlash(srv, "admin", path)
576 maxMsgSize := l.SMTPMaxMessageSize
578 maxMsgSize = config.DefaultMaxMsgSize
580 if l.WebmailHTTP.Enabled {
581 port := config.Port(l.WebmailHTTP.Port, 80)
583 if l.WebmailHTTP.Path != "" {
584 path = l.WebmailHTTP.Path
586 srv := ensureServe(false, port, "webmail-http at "+path)
587 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded)))
588 srv.Handle("webmail", nil, path, handler)
589 redirectToTrailingSlash(srv, "webmail", path)
591 if l.WebmailHTTPS.Enabled {
592 port := config.Port(l.WebmailHTTPS.Port, 443)
594 if l.WebmailHTTPS.Path != "" {
595 path = l.WebmailHTTPS.Path
597 srv := ensureServe(true, port, "webmail-https at "+path)
598 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded)))
599 srv.Handle("webmail", nil, path, handler)
600 redirectToTrailingSlash(srv, "webmail", path)
603 if l.MetricsHTTP.Enabled {
604 port := config.Port(l.MetricsHTTP.Port, 8010)
605 srv := ensureServe(false, port, "metrics-http")
606 srv.Handle("metrics", nil, "/metrics", safeHeaders(promhttp.Handler()))
607 srv.Handle("metrics", nil, "/", safeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
608 if r.URL.Path != "/" {
611 } else if r.Method != "GET" {
612 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
615 w.Header().Set("Content-Type", "text/html")
616 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
619 if l.AutoconfigHTTPS.Enabled {
620 port := config.Port(l.AutoconfigHTTPS.Port, 443)
621 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https")
622 autoconfigMatch := func(dom dns.Domain) bool {
623 // Thunderbird requests an autodiscovery URL at the email address domain name, so
624 // autoconfig prefix is optional.
625 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
626 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
627 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
629 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
630 // use the mail server's host name.
631 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
634 dc, ok := mox.Conf.Domain(dom)
635 return ok && !dc.ReportsOnly
637 srv.Handle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", safeHeaders(http.HandlerFunc(autoconfHandle)))
638 srv.Handle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", safeHeaders(http.HandlerFunc(autodiscoverHandle)))
639 srv.Handle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", safeHeaders(http.HandlerFunc(mobileconfigHandle)))
640 srv.Handle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", safeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
642 if l.MTASTSHTTPS.Enabled {
643 port := config.Port(l.MTASTSHTTPS.Port, 443)
644 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https")
645 mtastsMatch := func(dom dns.Domain) bool {
646 // todo: may want to check this against the configured domains, could in theory be just a webserver.
647 return strings.HasPrefix(dom.ASCII, "mta-sts.")
649 srv.Handle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", safeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
651 if l.PprofHTTP.Enabled {
652 // Importing net/http/pprof registers handlers on the default serve mux.
653 port := config.Port(l.PprofHTTP.Port, 8011)
654 if _, ok := portServe[port]; ok {
655 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
657 srv := &serve{[]string{"pprof-http"}, nil, nil, false}
658 portServe[port] = srv
659 srv.Handle("pprof", nil, "/", http.DefaultServeMux)
661 if l.WebserverHTTP.Enabled {
662 port := config.Port(l.WebserverHTTP.Port, 80)
663 srv := ensureServe(false, port, "webserver-http")
666 if l.WebserverHTTPS.Enabled {
667 port := config.Port(l.WebserverHTTPS.Port, 443)
668 srv := ensureServe(true, port, "webserver-https")
672 if l.TLS != nil && l.TLS.ACME != "" {
673 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
675 // If we are listening on port 80 for plain http, also register acme http-01
676 // validation handler.
677 if srv, ok := portServe[80]; ok && srv.TLSConfig == nil {
678 srv.Kinds = append(srv.Kinds, "acme-http-01")
679 srv.Handle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
682 hosts := map[dns.Domain]struct{}{
683 mox.Conf.Static.HostnameDomain: {},
685 if l.HostnameDomain.ASCII != "" {
686 hosts[l.HostnameDomain] = struct{}{}
688 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
689 // presence of TLS certificates for.
690 for _, name := range mox.Conf.Domains() {
691 if dom, err := dns.ParseDomain(name); err != nil {
692 pkglog.Errorx("parsing domain from config", err)
693 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
694 // Do not gather autoconfig name if we aren't accepting email for this domain.
698 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
700 pkglog.Errorx("parsing domain from config for autoconfig", err)
702 hosts[autoconfdom] = struct{}{}
706 ensureManagerHosts[m] = hosts
709 ports := maps.Keys(portServe)
711 for _, port := range ports {
712 srv := portServe[port]
713 sort.Slice(srv.PathHandlers, func(i, j int) bool {
714 a := srv.PathHandlers[i].Path
715 b := srv.PathHandlers[j].Path
716 if len(a) == len(b) {
717 // For consistent order.
720 // Longest paths first.
721 return len(a) > len(b)
723 for _, ip := range l.IPs {
724 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv)
730// functions to be launched in goroutine that will serve on a listener.
733// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
734// immediately after startup. We only do so for our explicit listener hostnames,
735// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
736// do request autoconfig, otherwise clients may run into their timeouts waiting for
737// the certificate to be given during the first https connection.
738var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
740// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
741func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler) {
742 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
747 if tlsConfig == nil {
749 if os.Getuid() == 0 {
750 pkglog.Print("http listener",
751 slog.String("name", name),
752 slog.String("kinds", strings.Join(kinds, ",")),
753 slog.String("address", addr))
755 ln, err = mox.Listen(mox.Network(ip), addr)
757 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
761 if os.Getuid() == 0 {
762 pkglog.Print("https listener",
763 slog.String("name", name),
764 slog.String("kinds", strings.Join(kinds, ",")),
765 slog.String("address", addr))
767 ln, err = mox.Listen(mox.Network(ip), addr)
769 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
771 ln = tls.NewListener(ln, tlsConfig)
774 server := &http.Server{
776 TLSConfig: tlsConfig,
777 ReadHeaderTimeout: 30 * time.Second,
778 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
779 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
782 err := server.Serve(ln)
783 pkglog.Fatalx(protocol+": serve", err)
785 servers = append(servers, serve)
788// Serve starts serving on the initialized listeners.
790 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
792 go webaccount.ImportManage()
794 for _, serve := range servers {
800 time.Sleep(1 * time.Second)
802 for m, hosts := range ensureManagerHosts {
803 for host := range hosts {
804 // Check if certificate is already available. If so, we don't print as much after a
805 // restart, and finish more quickly if only a few certificates are missing/old.
806 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
807 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
813 // Just in case someone adds quite some domains to their config. We don't want to
814 // hit any ACME rate limits.
818 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
819 time.Sleep(10 * time.Second)
823 hello := &tls.ClientHelloInfo{
824 ServerName: host.ASCII,
826 // Make us fetch an ECDSA P256 cert.
827 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
828 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
829 SupportedCurves: []tls.CurveID{tls.CurveP256},
830 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
831 SupportedVersions: []uint16{tls.VersionTLS13},
833 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
834 if _, err := m.Manager.GetCertificate(hello); err != nil {
835 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))