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 "maps"
15 "net"
16 "net/http"
17 "os"
18 "path"
19 "slices"
20 "sort"
21 "strings"
22 "time"
23
24 _ "embed"
25 _ "net/http/pprof"
26
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, strings.TrimRight(path, "/"), 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 := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
549 for _, name := range names {
550 l := mox.Conf.Static.Listeners[name]
551 portServe := portServes(name, l)
552
553 ports := slices.Sorted(maps.Keys(portServe))
554 for _, port := range ports {
555 srv := portServe[port]
556 for _, ip := range l.IPs {
557 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, srv.NextProto)
558 }
559 }
560 }
561}
562
563func portServes(name string, l config.Listener) map[int]*serve {
564 portServe := map[int]*serve{}
565
566 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
567 localhost := dns.Domain{ASCII: "localhost"}
568
569 ldom := l.HostnameDomain
570 if l.Hostname == "" {
571 ldom = mox.Conf.Static.HostnameDomain
572 }
573 listenerHostMatch := func(host dns.IPDomain) bool {
574 if host.IsIP() {
575 return true
576 }
577 return host.Domain == ldom || host.Domain == localhost
578 }
579 accountHostMatch := func(host dns.IPDomain) bool {
580 if listenerHostMatch(host) {
581 return true
582 }
583 return mox.Conf.IsClientSettingsDomain(host.Domain)
584 }
585
586 var ensureServe func(https bool, port int, kind string, favicon bool) *serve
587 ensureServe = func(https bool, port int, kind string, favicon bool) *serve {
588 s := portServe[port]
589 if s == nil {
590 s = &serve{nil, nil, tlsNextProtoMap{}, false, nil, false, nil}
591 portServe[port] = s
592 }
593 s.Kinds = append(s.Kinds, kind)
594 if favicon && !s.Favicon {
595 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
596 s.Favicon = true
597 }
598
599 // We clone TLS configs because we may modify it later on for this server, for
600 // ALPN. And we need copies because multiple listeners on http.Server where the
601 // config is used will try to modify it concurrently.
602 if https && l.TLS.ACME != "" {
603 s.TLSConfig = l.TLS.ACMEConfig.Clone()
604
605 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
606 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
607 ensureServe(true, tlsport, "acme-tls-alpn-01", false)
608 }
609 } else if https {
610 s.TLSConfig = l.TLS.Config.Clone()
611 }
612 return s
613 }
614
615 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
616 // yet, add http-01 validation mechanism handler to server.
617 ensureACMEHTTP01 := func(srv *serve) {
618 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
619 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
620 srv.Kinds = append(srv.Kinds, "acme-http-01")
621 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
622 }
623 }
624
625 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
626 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
627 ensureServe(true, port, "acme-tls-alpn-01", false)
628 }
629 if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
630 s := ensureServe(true, 443, "smtp-https", false)
631 hostname := mox.Conf.Static.HostnameDomain
632 if l.Hostname != "" {
633 hostname = l.HostnameDomain
634 }
635
636 maxMsgSize := l.SMTPMaxMessageSize
637 if maxMsgSize == 0 {
638 maxMsgSize = config.DefaultMaxMsgSize
639 }
640 requireTLS := !l.SMTP.NoRequireTLS
641
642 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
643 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
644 }
645 }
646 if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
647 s := ensureServe(true, 443, "imap-https", false)
648 s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
649 imapserver.ServeTLSConn(name, conn, s.TLSConfig)
650 }
651 }
652 if l.AccountHTTP.Enabled {
653 port := config.Port(l.AccountHTTP.Port, 80)
654 path := "/"
655 if l.AccountHTTP.Path != "" {
656 path = l.AccountHTTP.Path
657 }
658 srv := ensureServe(false, port, "account-http at "+path, true)
659 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
660 srv.ServiceHandle("account", accountHostMatch, path, handler)
661 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
662 ensureACMEHTTP01(srv)
663 }
664 if l.AccountHTTPS.Enabled {
665 port := config.Port(l.AccountHTTPS.Port, 443)
666 path := "/"
667 if l.AccountHTTPS.Path != "" {
668 path = l.AccountHTTPS.Path
669 }
670 srv := ensureServe(true, port, "account-https at "+path, true)
671 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
672 srv.ServiceHandle("account", accountHostMatch, path, handler)
673 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
674 }
675
676 if l.AdminHTTP.Enabled {
677 port := config.Port(l.AdminHTTP.Port, 80)
678 path := "/admin/"
679 if l.AdminHTTP.Path != "" {
680 path = l.AdminHTTP.Path
681 }
682 srv := ensureServe(false, port, "admin-http at "+path, true)
683 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
684 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
685 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
686 ensureACMEHTTP01(srv)
687 }
688 if l.AdminHTTPS.Enabled {
689 port := config.Port(l.AdminHTTPS.Port, 443)
690 path := "/admin/"
691 if l.AdminHTTPS.Path != "" {
692 path = l.AdminHTTPS.Path
693 }
694 srv := ensureServe(true, port, "admin-https at "+path, true)
695 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
696 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
697 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
698 }
699
700 maxMsgSize := l.SMTPMaxMessageSize
701 if maxMsgSize == 0 {
702 maxMsgSize = config.DefaultMaxMsgSize
703 }
704
705 if l.WebAPIHTTP.Enabled {
706 port := config.Port(l.WebAPIHTTP.Port, 80)
707 path := "/webapi/"
708 if l.WebAPIHTTP.Path != "" {
709 path = l.WebAPIHTTP.Path
710 }
711 srv := ensureServe(false, port, "webapi-http at "+path, true)
712 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
713 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
714 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
715 ensureACMEHTTP01(srv)
716 }
717 if l.WebAPIHTTPS.Enabled {
718 port := config.Port(l.WebAPIHTTPS.Port, 443)
719 path := "/webapi/"
720 if l.WebAPIHTTPS.Path != "" {
721 path = l.WebAPIHTTPS.Path
722 }
723 srv := ensureServe(true, port, "webapi-https at "+path, true)
724 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
725 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
726 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
727 }
728
729 if l.WebmailHTTP.Enabled {
730 port := config.Port(l.WebmailHTTP.Port, 80)
731 path := "/webmail/"
732 if l.WebmailHTTP.Path != "" {
733 path = l.WebmailHTTP.Path
734 }
735 srv := ensureServe(false, port, "webmail-http at "+path, true)
736 var accountPath string
737 if l.AccountHTTP.Enabled {
738 accountPath = "/"
739 if l.AccountHTTP.Path != "" {
740 accountPath = l.AccountHTTP.Path
741 }
742 }
743 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
744 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
745 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
746 ensureACMEHTTP01(srv)
747 }
748 if l.WebmailHTTPS.Enabled {
749 port := config.Port(l.WebmailHTTPS.Port, 443)
750 path := "/webmail/"
751 if l.WebmailHTTPS.Path != "" {
752 path = l.WebmailHTTPS.Path
753 }
754 srv := ensureServe(true, port, "webmail-https at "+path, true)
755 var accountPath string
756 if l.AccountHTTPS.Enabled {
757 accountPath = "/"
758 if l.AccountHTTPS.Path != "" {
759 accountPath = l.AccountHTTPS.Path
760 }
761 }
762 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
763 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
764 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
765 }
766
767 if l.MetricsHTTP.Enabled {
768 port := config.Port(l.MetricsHTTP.Port, 8010)
769 srv := ensureServe(false, port, "metrics-http", false)
770 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
771 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
772 if r.URL.Path != "/" {
773 http.NotFound(w, r)
774 return
775 } else if r.Method != "GET" {
776 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
777 return
778 }
779 w.Header().Set("Content-Type", "text/html")
780 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
781 })))
782 }
783 if l.AutoconfigHTTPS.Enabled {
784 port := config.Port(l.AutoconfigHTTPS.Port, 443)
785 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https", false)
786 if l.AutoconfigHTTPS.NonTLS {
787 ensureACMEHTTP01(srv)
788 }
789 autoconfigMatch := func(ipdom dns.IPDomain) bool {
790 dom := ipdom.Domain
791 if dom.IsZero() {
792 return false
793 }
794 // Thunderbird requests an autodiscovery URL at the email address domain name, so
795 // autoconfig prefix is optional.
796 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
797 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
798 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
799 }
800 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
801 // use the mail server's host name.
802 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
803 return true
804 }
805 dc, ok := mox.Conf.Domain(dom)
806 return ok && !dc.ReportsOnly
807 }
808 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
809 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
810 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
811 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
812 }
813 if l.MTASTSHTTPS.Enabled {
814 port := config.Port(l.MTASTSHTTPS.Port, 443)
815 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https", false)
816 if l.MTASTSHTTPS.NonTLS {
817 ensureACMEHTTP01(srv)
818 }
819 mtastsMatch := func(ipdom dns.IPDomain) bool {
820 // todo: may want to check this against the configured domains, could in theory be just a webserver.
821 dom := ipdom.Domain
822 if dom.IsZero() {
823 return false
824 }
825 return strings.HasPrefix(dom.ASCII, "mta-sts.")
826 }
827 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
828 }
829 if l.PprofHTTP.Enabled {
830 // Importing net/http/pprof registers handlers on the default serve mux.
831 port := config.Port(l.PprofHTTP.Port, 8011)
832 if _, ok := portServe[port]; ok {
833 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
834 }
835 srv := &serve{[]string{"pprof-http"}, nil, nil, false, nil, false, nil}
836 portServe[port] = srv
837 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
838 }
839 if l.WebserverHTTP.Enabled {
840 port := config.Port(l.WebserverHTTP.Port, 80)
841 srv := ensureServe(false, port, "webserver-http", false)
842 srv.Webserver = true
843 ensureACMEHTTP01(srv)
844 }
845 if l.WebserverHTTPS.Enabled {
846 port := config.Port(l.WebserverHTTPS.Port, 443)
847 srv := ensureServe(true, port, "webserver-https", false)
848 srv.Webserver = true
849 }
850
851 if l.TLS != nil && l.TLS.ACME != "" {
852 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
853 if ensureManagerHosts[m] == nil {
854 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
855 }
856 hosts := ensureManagerHosts[m]
857 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
858
859 if l.HostnameDomain.ASCII != "" {
860 hosts[l.HostnameDomain] = struct{}{}
861 }
862
863 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
864 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
865 // for the timeouts of clients doing autoconfig.
866
867 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
868 for _, name := range mox.Conf.Domains() {
869 if dom, err := dns.ParseDomain(name); err != nil {
870 pkglog.Errorx("parsing domain from config", err)
871 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly || d.Disabled {
872 // Do not gather autoconfig name if we aren't accepting email for this domain or when it is disabled.
873 continue
874 }
875
876 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
877 if err != nil {
878 pkglog.Errorx("parsing domain from config for autoconfig", err)
879 } else {
880 hosts[autoconfdom] = struct{}{}
881 }
882 }
883 }
884 }
885
886 if s := portServe[443]; s != nil && s.TLSConfig != nil && len(s.NextProto) > 0 {
887 s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, slices.Collect(maps.Keys(s.NextProto))...)
888 }
889
890 for _, srv := range portServe {
891 sortPathHandlers(srv.SystemHandlers)
892 sortPathHandlers(srv.ServiceHandlers)
893 }
894
895 return portServe
896}
897
898func sortPathHandlers(l []pathHandler) {
899 sort.Slice(l, func(i, j int) bool {
900 a := l[i].Path
901 b := l[j].Path
902 if len(a) == len(b) {
903 // For consistent order.
904 return a < b
905 }
906 // Longest paths first.
907 return len(a) > len(b)
908 })
909}
910
911// functions to be launched in goroutine that will serve on a listener.
912var servers []func()
913
914// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
915// immediately after startup. We only do so for our explicit listener hostnames,
916// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
917// do request autoconfig, otherwise clients may run into their timeouts waiting for
918// the certificate to be given during the first https connection.
919var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
920
921type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
922
923// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
924func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, nextProto tlsNextProtoMap) {
925 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
926
927 var protocol string
928 var ln net.Listener
929 var err error
930 if tlsConfig == nil {
931 protocol = "http"
932 if os.Getuid() == 0 {
933 pkglog.Print("http listener",
934 slog.String("name", name),
935 slog.String("kinds", strings.Join(kinds, ",")),
936 slog.String("address", addr))
937 }
938 ln, err = mox.Listen(mox.Network(ip), addr)
939 if err != nil {
940 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
941 }
942 } else {
943 protocol = "https"
944 if os.Getuid() == 0 {
945 pkglog.Print("https listener",
946 slog.String("name", name),
947 slog.String("kinds", strings.Join(kinds, ",")),
948 slog.String("address", addr))
949 }
950 ln, err = mox.Listen(mox.Network(ip), addr)
951 if err != nil {
952 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
953 }
954 ln = tls.NewListener(ln, tlsConfig)
955 }
956
957 server := &http.Server{
958 Handler: handler,
959 TLSConfig: tlsConfig,
960 ReadHeaderTimeout: 30 * time.Second,
961 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
962 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
963 TLSNextProto: nextProto,
964 }
965 // By default, the Go 1.6 and above http.Server includes support for HTTP2.
966 // However, HTTP2 is negotiated via ALPN. Because we are configuring
967 // TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
968 // and calling ConfigureServer.
969 err = http2.ConfigureServer(server, nil)
970 if err != nil {
971 pkglog.Fatalx("https: unable to configure http2", err)
972 }
973 serve := func() {
974 err := server.Serve(ln)
975 pkglog.Fatalx(protocol+": serve", err)
976 }
977 servers = append(servers, serve)
978}
979
980// Serve starts serving on the initialized listeners.
981func Serve() {
982 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
983
984 go webaccount.ImportManage()
985
986 for _, serve := range servers {
987 go serve()
988 }
989 servers = nil
990
991 go func() {
992 time.Sleep(1 * time.Second)
993 i := 0
994 for m, hosts := range ensureManagerHosts {
995 for host := range hosts {
996 // Check if certificate is already available. If so, we don't print as much after a
997 // restart, and finish more quickly if only a few certificates are missing/old.
998 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
999 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
1000 } else if avail {
1001 continue
1002 }
1003
1004 if i >= 10 {
1005 // Just in case someone adds quite some domains to their config. We don't want to
1006 // hit any ACME rate limits.
1007 return
1008 }
1009 if i > 0 {
1010 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
1011 time.Sleep(10 * time.Second)
1012 }
1013 i++
1014
1015 hello := &tls.ClientHelloInfo{
1016 ServerName: host.ASCII,
1017
1018 // Make us fetch an ECDSA P256 cert.
1019 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
1020 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
1021 SupportedCurves: []tls.CurveID{tls.CurveP256},
1022 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
1023 SupportedVersions: []uint16{tls.VersionTLS13},
1024 }
1025 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
1026 if _, err := m.Manager.GetCertificate(hello); err != nil {
1027 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))
1028 }
1029 }
1030 }
1031 }()
1032}
1033