1// Package http provides HTTP listeners/servers, for
2// autoconfiguration/autodiscovery, the account and admin web interface and
3// MTA-STS policies.
4package http
5
6import (
7 "compress/gzip"
8 "context"
9 "crypto/tls"
10 "fmt"
11 "io"
12 golog "log"
13 "log/slog"
14 "net"
15 "net/http"
16 "os"
17 "path"
18 "slices"
19 "sort"
20 "strings"
21 "time"
22
23 _ "embed"
24 _ "net/http/pprof"
25
26 "golang.org/x/exp/maps"
27 "golang.org/x/net/http2"
28
29 "github.com/prometheus/client_golang/prometheus"
30 "github.com/prometheus/client_golang/prometheus/promauto"
31 "github.com/prometheus/client_golang/prometheus/promhttp"
32
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"
45)
46
47var pkglog = mlog.New("http", nil)
48
49var (
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},
56 },
57 []string{
58 "handler", // Name from webhandler, can be empty.
59 "proto", // "http", "https", "ws", "wss"
60 "method", // "(unknown)" and otherwise only common verbs
61 "code",
62 },
63 )
64 // metricResponse tracks performance of entire request as experienced by users,
65 // which also depends on their connection speed, so not necessarily something you
66 // could act on.
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},
72 },
73 []string{
74 "handler", // Name from webhandler, can be empty.
75 "proto", // "http", "https", "ws", "wss"
76 "method", // "(unknown)" and otherwise only common verbs
77 "code",
78 },
79 )
80)
81
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.
87//
88//go:embed favicon.ico
89var faviconIco string
90var faviconModTime = time.Now()
91
92func init() {
93 p, err := os.Executable()
94 if err == nil {
95 if st, err := os.Stat(p); err == nil {
96 faviconModTime = st.ModTime()
97 }
98 }
99}
100
101func faviconHandle(w http.ResponseWriter, r *http.Request) {
102 http.ServeContent(w, r, "favicon.ico", faviconModTime, strings.NewReader(faviconIco))
103}
104
105type responseWriterFlusher interface {
106 http.ResponseWriter
107 http.Flusher
108}
109
110// http.ResponseWriter that writes access log and tracks metrics at end of response.
111type loggingWriter struct {
112 W responseWriterFlusher // Calls are forwarded.
113 Start time.Time
114 R *http.Request
115 WebsocketRequest bool // Whether request from was websocket.
116
117 // Set by router.
118 Handler string
119 Compress bool
120
121 // Set by handlers.
122 StatusCode int
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).
126 Err error
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.
130}
131
132func (w *loggingWriter) AddAttr(a slog.Attr) {
133 w.Attrs = append(w.Attrs, a)
134}
135
136func (w *loggingWriter) Flush() {
137 w.W.Flush()
138}
139
140func (w *loggingWriter) Header() http.Header {
141 return w.W.Header()
142}
143
144// protocol, for logging.
145func (w *loggingWriter) proto(websocket bool) string {
146 proto := "http"
147 if websocket {
148 proto = "ws"
149 }
150 if w.R.TLS != nil {
151 proto += "s"
152 }
153 return proto
154}
155
156func (w *loggingWriter) Write(buf []byte) (int, error) {
157 if w.StatusCode == 0 {
158 w.WriteHeader(http.StatusOK)
159 }
160
161 var n int
162 var err error
163 if w.Gzip == nil {
164 n, err = w.W.Write(buf)
165 if n > 0 {
166 w.Size += int64(n)
167 }
168 } else {
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)
173 if err == nil {
174 err = w.Gzip.Flush()
175 }
176 if n > 0 {
177 w.UncompressedSize += int64(n)
178 }
179 }
180 if err != nil {
181 w.error(err)
182 }
183 return n, err
184}
185
186func (w *loggingWriter) setStatusCode(statusCode int) {
187 if w.StatusCode != 0 {
188 return
189 }
190
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))
194}
195
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
200}
201
202func (w *loggingWriter) WriteHeader(statusCode int) {
203 if w.StatusCode != 0 {
204 return
205 }
206
207 w.setStatusCode(statusCode)
208
209 // We transparently gzip-compress responses for requests under these conditions, all must apply:
210 //
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.
219
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.
225 }
226 w.W.WriteHeader(statusCode)
227}
228
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" {
236 continue
237 }
238 if tt[0] == "gzip" {
239 return true
240 }
241 }
242 return false
243}
244
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,
253 "font/ttf": true,
254 "font/eot": true,
255 "font/otf": true,
256 "font/opentype": true,
257}
258
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] {
264 return true
265 }
266 t, st, _ := strings.Cut(ct, "/")
267 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
268}
269
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 {
273 return false
274 }
275
276 buf := make([]byte, 512)
277 n, err := f.ReadAt(buf, 0)
278 if err != nil && err != io.EOF {
279 return false
280 }
281 ct := http.DetectContentType(buf[:n])
282 return compressibleContentType(ct)
283}
284
285type countWriter struct {
286 Writer io.Writer
287 Size *int64
288}
289
290func (w countWriter) Write(buf []byte) (int, error) {
291 n, err := w.Writer.Write(buf)
292 if n > 0 {
293 *w.Size += int64(n)
294 }
295 return n, err
296}
297
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",
303}
304
305func metricHTTPMethod(method string) string {
306 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
307 method = strings.ToLower(method)
308 switch 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":
310 return method
311 }
312 return "(other)"
313}
314
315func (w *loggingWriter) error(err error) {
316 if w.Err == nil {
317 w.Err = err
318 }
319}
320
321func (w *loggingWriter) Done() {
322 if w.Err == nil && w.Gzip != nil {
323 if err := w.Gzip.Close(); err != nil {
324 w.error(err)
325 }
326 }
327
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))
330
331 tlsinfo := "plain"
332 if w.R.TLS != nil {
333 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
334 tlsinfo = v
335 } else {
336 tlsinfo = "(other)"
337 }
338 }
339 err := w.Err
340 if err == nil {
341 err = w.R.Context().Err()
342 }
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")),
356 }
357 if w.WebsocketRequest {
358 attrs = append(attrs,
359 slog.Bool("websocketrequest", true),
360 )
361 }
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),
367 )
368 } else if w.UncompressedSize > 0 {
369 attrs = append(attrs,
370 slog.Int64("size", w.Size),
371 slog.Int64("uncompressedsize", w.UncompressedSize),
372 )
373 } else {
374 attrs = append(attrs,
375 slog.Int64("size", w.Size),
376 )
377 }
378 attrs = append(attrs, w.Attrs...)
379 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
380}
381
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.
387 Handler http.Handler
388}
389type serve struct {
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.
393 Favicon bool
394
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.
402 Webserver bool
403 ServiceHandlers []pathHandler // Sorted, longest first.
404}
405
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
409// handler.
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})
412}
413
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})
418}
419
420var (
421 limiterConnectionrate = &ratelimit.Limiter{
422 WindowLimits: []ratelimit.WindowLimit{
423 {
424 Window: time.Minute,
425 Limits: [...]int64{1000, 3000, 9000},
426 },
427 {
428 Window: time.Hour,
429 Limits: [...]int64{5000, 15000, 45000},
430 },
431 },
432 }
433)
434
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
437// metrics.
438func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
439 now := time.Now()
440 // Rate limiting as early as possible.
441 ipstr, _, err := net.SplitHostPort(r.RemoteAddr)
442 if err != nil {
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)
448 proto := "http"
449 if r.TLS != nil {
450 proto = "https"
451 }
452 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
453 // No logging, that's just noise.
454
455 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
456 return
457 }
458
459 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
460 r = r.WithContext(ctx)
461
462 wf, ok := xw.(responseWriterFlusher)
463 if !ok {
464 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
465 return
466 }
467
468 nw := &loggingWriter{
469 W: wf,
470 Start: now,
471 R: r,
472 }
473 defer nw.Done()
474
475 // Cleanup path, removing ".." and ".". Keep any trailing slash.
476 trailingPath := strings.HasSuffix(r.URL.Path, "/")
477 if r.URL.Path == "" {
478 r.URL.Path = "/"
479 }
480 r.URL.Path = path.Clean(r.URL.Path)
481 if r.URL.Path == "." {
482 r.URL.Path = "/"
483 }
484 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
485 r.URL.Path += "/"
486 }
487
488 host := r.Host
489 nhost, _, err := net.SplitHostPort(host)
490 if err == nil {
491 host = nhost
492 }
493 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
494 if ipdom.IP == nil {
495 dom, domErr := dns.ParseDomain(host)
496 if domErr == nil {
497 ipdom = dns.IPDomain{Domain: dom}
498 }
499 }
500
501 handle := func(h pathHandler) bool {
502 if h.HostMatch != nil && !h.HostMatch(ipdom) {
503 return false
504 }
505 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
506 nw.Handler = h.Name
507 nw.Compress = true
508 h.Handler.ServeHTTP(nw, r)
509 return true
510 }
511 return false
512 }
513
514 for _, h := range s.SystemHandlers {
515 if handle(h) {
516 return
517 }
518 }
519 if s.Webserver {
520 if WebHandle(nw, r, ipdom) {
521 return
522 }
523 }
524 for _, h := range s.ServiceHandlers {
525 if handle(h) {
526 return
527 }
528 }
529 nw.Handler = "(nomatch)"
530 http.NotFound(nw, r)
531}
532
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)
538 }))
539 srv.ServiceHandle(name, hostMatch, path[:len(path)-1], handler)
540 }
541}
542
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.
545func Listen() {
546 // Initialize listeners in deterministic order for the same potential error
547 // messages.
548 names := maps.Keys(mox.Conf.Static.Listeners)
549 sort.Strings(names)
550 for _, name := range names {
551 l := mox.Conf.Static.Listeners[name]
552 portServe := portServes(name, l)
553
554 ports := maps.Keys(portServe)
555 sort.Ints(ports)
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)
560 }
561 }
562 }
563}
564
565func portServes(name string, l config.Listener) map[int]*serve {
566 portServe := map[int]*serve{}
567
568 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
569 localhost := dns.Domain{ASCII: "localhost"}
570
571 ldom := l.HostnameDomain
572 if l.Hostname == "" {
573 ldom = mox.Conf.Static.HostnameDomain
574 }
575 listenerHostMatch := func(host dns.IPDomain) bool {
576 if host.IsIP() {
577 return true
578 }
579 return host.Domain == ldom || host.Domain == localhost
580 }
581 accountHostMatch := func(host dns.IPDomain) bool {
582 if listenerHostMatch(host) {
583 return true
584 }
585 return mox.Conf.IsClientSettingsDomain(host.Domain)
586 }
587
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 {
590 s := portServe[port]
591 if s == nil {
592 s = &serve{nil, nil, tlsNextProtoMap{}, false, nil, false, nil}
593 portServe[port] = s
594 }
595 s.Kinds = append(s.Kinds, kind)
596 if favicon && !s.Favicon {
597 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
598 s.Favicon = true
599 }
600
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()
606
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)
610 }
611 } else if https {
612 s.TLSConfig = l.TLS.Config.Clone()
613 }
614 return s
615 }
616
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))
624 }
625 }
626
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)
630 }
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
636 }
637
638 maxMsgSize := l.SMTPMaxMessageSize
639 if maxMsgSize == 0 {
640 maxMsgSize = config.DefaultMaxMsgSize
641 }
642 requireTLS := !l.SMTP.NoRequireTLS
643
644 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
645 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
646 }
647 }
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)
652 }
653 }
654 if l.AccountHTTP.Enabled {
655 port := config.Port(l.AccountHTTP.Port, 80)
656 path := "/"
657 if l.AccountHTTP.Path != "" {
658 path = l.AccountHTTP.Path
659 }
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)
665 }
666 if l.AccountHTTPS.Enabled {
667 port := config.Port(l.AccountHTTPS.Port, 443)
668 path := "/"
669 if l.AccountHTTPS.Path != "" {
670 path = l.AccountHTTPS.Path
671 }
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)
676 }
677
678 if l.AdminHTTP.Enabled {
679 port := config.Port(l.AdminHTTP.Port, 80)
680 path := "/admin/"
681 if l.AdminHTTP.Path != "" {
682 path = l.AdminHTTP.Path
683 }
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)
689 }
690 if l.AdminHTTPS.Enabled {
691 port := config.Port(l.AdminHTTPS.Port, 443)
692 path := "/admin/"
693 if l.AdminHTTPS.Path != "" {
694 path = l.AdminHTTPS.Path
695 }
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)
700 }
701
702 maxMsgSize := l.SMTPMaxMessageSize
703 if maxMsgSize == 0 {
704 maxMsgSize = config.DefaultMaxMsgSize
705 }
706
707 if l.WebAPIHTTP.Enabled {
708 port := config.Port(l.WebAPIHTTP.Port, 80)
709 path := "/webapi/"
710 if l.WebAPIHTTP.Path != "" {
711 path = l.WebAPIHTTP.Path
712 }
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)
718 }
719 if l.WebAPIHTTPS.Enabled {
720 port := config.Port(l.WebAPIHTTPS.Port, 443)
721 path := "/webapi/"
722 if l.WebAPIHTTPS.Path != "" {
723 path = l.WebAPIHTTPS.Path
724 }
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)
729 }
730
731 if l.WebmailHTTP.Enabled {
732 port := config.Port(l.WebmailHTTP.Port, 80)
733 path := "/webmail/"
734 if l.WebmailHTTP.Path != "" {
735 path = l.WebmailHTTP.Path
736 }
737 srv := ensureServe(false, port, "webmail-http at "+path, true)
738 var accountPath string
739 if l.AccountHTTP.Enabled {
740 accountPath = "/"
741 if l.AccountHTTP.Path != "" {
742 accountPath = l.AccountHTTP.Path
743 }
744 }
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)
749 }
750 if l.WebmailHTTPS.Enabled {
751 port := config.Port(l.WebmailHTTPS.Port, 443)
752 path := "/webmail/"
753 if l.WebmailHTTPS.Path != "" {
754 path = l.WebmailHTTPS.Path
755 }
756 srv := ensureServe(true, port, "webmail-https at "+path, true)
757 var accountPath string
758 if l.AccountHTTPS.Enabled {
759 accountPath = "/"
760 if l.AccountHTTPS.Path != "" {
761 accountPath = l.AccountHTTPS.Path
762 }
763 }
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)
767 }
768
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 != "/" {
775 http.NotFound(w, r)
776 return
777 } else if r.Method != "GET" {
778 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
779 return
780 }
781 w.Header().Set("Content-Type", "text/html")
782 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
783 })))
784 }
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)
790 }
791 autoconfigMatch := func(ipdom dns.IPDomain) bool {
792 dom := ipdom.Domain
793 if dom.IsZero() {
794 return false
795 }
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.")
801 }
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 {
805 return true
806 }
807 dc, ok := mox.Conf.Domain(dom)
808 return ok && !dc.ReportsOnly
809 }
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)))
814 }
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)
820 }
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.
823 dom := ipdom.Domain
824 if dom.IsZero() {
825 return false
826 }
827 return strings.HasPrefix(dom.ASCII, "mta-sts.")
828 }
829 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
830 }
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")
836 }
837 srv := &serve{[]string{"pprof-http"}, nil, nil, false, nil, false, nil}
838 portServe[port] = srv
839 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
840 }
841 if l.WebserverHTTP.Enabled {
842 port := config.Port(l.WebserverHTTP.Port, 80)
843 srv := ensureServe(false, port, "webserver-http", false)
844 srv.Webserver = true
845 ensureACMEHTTP01(srv)
846 }
847 if l.WebserverHTTPS.Enabled {
848 port := config.Port(l.WebserverHTTPS.Port, 443)
849 srv := ensureServe(true, port, "webserver-https", false)
850 srv.Webserver = true
851 }
852
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{}{}
857 }
858 hosts := ensureManagerHosts[m]
859 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
860
861 if l.HostnameDomain.ASCII != "" {
862 hosts[l.HostnameDomain] = struct{}{}
863 }
864
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.
868
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.
875 continue
876 }
877
878 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
879 if err != nil {
880 pkglog.Errorx("parsing domain from config for autoconfig", err)
881 } else {
882 hosts[autoconfdom] = struct{}{}
883 }
884 }
885 }
886 }
887
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)...)
890 }
891
892 for _, srv := range portServe {
893 sortPathHandlers(srv.SystemHandlers)
894 sortPathHandlers(srv.ServiceHandlers)
895 }
896
897 return portServe
898}
899
900func sortPathHandlers(l []pathHandler) {
901 sort.Slice(l, func(i, j int) bool {
902 a := l[i].Path
903 b := l[j].Path
904 if len(a) == len(b) {
905 // For consistent order.
906 return a < b
907 }
908 // Longest paths first.
909 return len(a) > len(b)
910 })
911}
912
913// functions to be launched in goroutine that will serve on a listener.
914var servers []func()
915
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{}{}
922
923type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
924
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))
928
929 var protocol string
930 var ln net.Listener
931 var err error
932 if tlsConfig == nil {
933 protocol = "http"
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))
939 }
940 ln, err = mox.Listen(mox.Network(ip), addr)
941 if err != nil {
942 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
943 }
944 } else {
945 protocol = "https"
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))
951 }
952 ln, err = mox.Listen(mox.Network(ip), addr)
953 if err != nil {
954 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
955 }
956 ln = tls.NewListener(ln, tlsConfig)
957 }
958
959 server := &http.Server{
960 Handler: handler,
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,
966 }
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)
972 if err != nil {
973 pkglog.Fatalx("https: unable to configure http2", err)
974 }
975 serve := func() {
976 err := server.Serve(ln)
977 pkglog.Fatalx(protocol+": serve", err)
978 }
979 servers = append(servers, serve)
980}
981
982// Serve starts serving on the initialized listeners.
983func Serve() {
984 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
985
986 go webaccount.ImportManage()
987
988 for _, serve := range servers {
989 go serve()
990 }
991 servers = nil
992
993 go func() {
994 time.Sleep(1 * time.Second)
995 i := 0
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))
1002 } else if avail {
1003 continue
1004 }
1005
1006 if i >= 10 {
1007 // Just in case someone adds quite some domains to their config. We don't want to
1008 // hit any ACME rate limits.
1009 return
1010 }
1011 if i > 0 {
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)
1014 }
1015 i++
1016
1017 hello := &tls.ClientHelloInfo{
1018 ServerName: host.ASCII,
1019
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},
1026 }
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))
1030 }
1031 }
1032 }
1033 }()
1034}
1035