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 Forwarded bool
116 WebsocketRequest bool // Whether request from was websocket.
117
118 // Set by router.
119 Handler string
120 Compress bool
121
122 // Set by handlers.
123 StatusCode int
124 Size int64 // Of data served to client, for non-websocket responses.
125 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
126 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
127 Err error
128 WebsocketResponse bool // If this was a successful websocket connection with backend.
129 SizeFromClient, SizeToClient int64 // Websocket data.
130 Attrs []slog.Attr // Additional fields to log.
131}
132
133func (w *loggingWriter) AddAttr(a slog.Attr) {
134 w.Attrs = append(w.Attrs, a)
135}
136
137func (w *loggingWriter) Flush() {
138 w.W.Flush()
139}
140
141func (w *loggingWriter) Header() http.Header {
142 return w.W.Header()
143}
144
145// protocol, for logging.
146func (w *loggingWriter) proto(websocket bool) string {
147 proto := "http"
148 if websocket {
149 proto = "ws"
150 }
151 if w.R.TLS != nil {
152 proto += "s"
153 }
154 return proto
155}
156
157func (w *loggingWriter) Write(buf []byte) (int, error) {
158 if w.StatusCode == 0 {
159 w.WriteHeader(http.StatusOK)
160 }
161
162 var n int
163 var err error
164 if w.Gzip == nil {
165 n, err = w.W.Write(buf)
166 if n > 0 {
167 w.Size += int64(n)
168 }
169 } else {
170 // We flush after each write. Probably takes a few more bytes, but prevents any
171 // issues due to buffering.
172 // w.Gzip.Write updates w.Size with the compressed byte count.
173 n, err = w.Gzip.Write(buf)
174 if err == nil {
175 err = w.Gzip.Flush()
176 }
177 if n > 0 {
178 w.UncompressedSize += int64(n)
179 }
180 }
181 if err != nil {
182 w.error(err)
183 }
184 return n, err
185}
186
187func (w *loggingWriter) setStatusCode(statusCode int) {
188 if w.StatusCode != 0 {
189 return
190 }
191
192 w.StatusCode = statusCode
193 method := metricHTTPMethod(w.R.Method)
194 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
195}
196
197// SetUncompressedSize is used through an interface by
198// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
199func (w *loggingWriter) SetUncompressedSize(origSize int64) {
200 w.UncompressedSize = origSize
201}
202
203func (w *loggingWriter) WriteHeader(statusCode int) {
204 if w.StatusCode != 0 {
205 return
206 }
207
208 w.setStatusCode(statusCode)
209
210 // We transparently gzip-compress responses for requests under these conditions, all must apply:
211 //
212 // - Enabled for handler (static handlers make their own decisions).
213 // - Not a websocket request.
214 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
215 // - Not already compressed, or any other Content-Encoding header (including "identity").
216 // - Client accepts gzip encoded responses.
217 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
218 if w.Compress && !w.WebsocketRequest && statusCode == http.StatusOK && w.W.Header().Values("Content-Encoding") == nil && acceptsGzip(w.R) && compressibleContentType(w.W.Header().Get("Content-Type")) {
219 // todo: we should gather the first kb of data, see if it is compressible. if not, just return original. should set timer so we flush if it takes too long to gather 1kb. for smaller data we shouldn't compress at all.
220
221 // We track the gzipped output for the access log.
222 cw := countWriter{Writer: w.W, Size: &w.Size}
223 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
224 w.W.Header().Set("Content-Encoding", "gzip")
225 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
226 }
227 w.W.WriteHeader(statusCode)
228}
229
230func acceptsGzip(r *http.Request) bool {
231 s := r.Header.Get("Accept-Encoding")
232 t := strings.Split(s, ",")
233 for _, e := range t {
234 e = strings.TrimSpace(e)
235 tt := strings.Split(e, ";")
236 if len(tt) > 1 && t[1] == "q=0" {
237 continue
238 }
239 if tt[0] == "gzip" {
240 return true
241 }
242 }
243 return false
244}
245
246var compressibleTypes = map[string]bool{
247 "application/csv": true,
248 "application/javascript": true,
249 "application/json": true,
250 "application/x-javascript": true,
251 "application/xml": true,
252 "image/vnd.microsoft.icon": true,
253 "image/x-icon": true,
254 "font/ttf": true,
255 "font/eot": true,
256 "font/otf": true,
257 "font/opentype": true,
258}
259
260func compressibleContentType(ct string) bool {
261 ct = strings.SplitN(ct, ";", 2)[0]
262 ct = strings.TrimSpace(ct)
263 ct = strings.ToLower(ct)
264 if compressibleTypes[ct] {
265 return true
266 }
267 t, st, _ := strings.Cut(ct, "/")
268 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
269}
270
271func compressibleContent(f *os.File) bool {
272 // We don't want to store many small files. They take up too much disk overhead.
273 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
274 return false
275 }
276
277 buf := make([]byte, 512)
278 n, err := f.ReadAt(buf, 0)
279 if err != nil && err != io.EOF {
280 return false
281 }
282 ct := http.DetectContentType(buf[:n])
283 return compressibleContentType(ct)
284}
285
286type countWriter struct {
287 Writer io.Writer
288 Size *int64
289}
290
291func (w countWriter) Write(buf []byte) (int, error) {
292 n, err := w.Writer.Write(buf)
293 if n > 0 {
294 *w.Size += int64(n)
295 }
296 return n, err
297}
298
299var tlsVersions = map[uint16]string{
300 tls.VersionTLS10: "tls1.0",
301 tls.VersionTLS11: "tls1.1",
302 tls.VersionTLS12: "tls1.2",
303 tls.VersionTLS13: "tls1.3",
304}
305
306func metricHTTPMethod(method string) string {
307 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
308 method = strings.ToLower(method)
309 switch method {
310 case "acl", "baseline-control", "bind", "checkin", "checkout", "connect", "copy", "delete", "get", "head", "label", "link", "lock", "merge", "mkactivity", "mkcalendar", "mkcol", "mkredirectref", "mkworkspace", "move", "options", "orderpatch", "patch", "post", "pri", "propfind", "proppatch", "put", "rebind", "report", "search", "trace", "unbind", "uncheckout", "unlink", "unlock", "update", "updateredirectref", "version-control":
311 return method
312 }
313 return "(other)"
314}
315
316func (w *loggingWriter) error(err error) {
317 if w.Err == nil {
318 w.Err = err
319 }
320}
321
322func (w *loggingWriter) Done() {
323 if w.Err == nil && w.Gzip != nil {
324 if err := w.Gzip.Close(); err != nil {
325 w.error(err)
326 }
327 }
328
329 method := metricHTTPMethod(w.R.Method)
330 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
331
332 tlsinfo := "plain"
333 if w.R.TLS != nil {
334 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
335 tlsinfo = v
336 } else {
337 tlsinfo = "(other)"
338 }
339 }
340 err := w.Err
341 if err == nil {
342 err = w.R.Context().Err()
343 }
344 attrs := []slog.Attr{
345 slog.String("httpaccess", ""),
346 slog.String("handler", w.Handler),
347 slog.String("method", method),
348 slog.Any("url", w.R.URL),
349 slog.String("host", w.R.Host),
350 slog.Duration("duration", time.Since(w.Start)),
351 slog.Int("statuscode", w.StatusCode),
352 slog.String("proto", strings.ToLower(w.R.Proto)),
353 slog.String("remoteaddr", w.R.RemoteAddr),
354 slog.String("tlsinfo", tlsinfo),
355 slog.String("useragent", w.R.Header.Get("User-Agent")),
356 slog.String("referer", w.R.Header.Get("Referer")),
357 }
358 if w.Forwarded {
359 s := w.R.Header.Get("X-Forwarded-For")
360 ipstr := strings.TrimSpace(strings.Split(s, ",")[0])
361 attrs = append(attrs,
362 slog.String("clientip", ipstr),
363 )
364 }
365
366 if w.WebsocketRequest {
367 attrs = append(attrs,
368 slog.Bool("websocketrequest", true),
369 )
370 }
371 if w.WebsocketResponse {
372 attrs = append(attrs,
373 slog.Bool("websocket", true),
374 slog.Int64("sizetoclient", w.SizeToClient),
375 slog.Int64("sizefromclient", w.SizeFromClient),
376 )
377 } else if w.UncompressedSize > 0 {
378 attrs = append(attrs,
379 slog.Int64("size", w.Size),
380 slog.Int64("uncompressedsize", w.UncompressedSize),
381 )
382 } else {
383 attrs = append(attrs,
384 slog.Int64("size", w.Size),
385 )
386 }
387 attrs = append(attrs, w.Attrs...)
388 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
389}
390
391// Built-in handlers, e.g. mta-sts and autoconfig.
392type pathHandler struct {
393 Name string // For logging/metrics.
394 HostMatch func(host dns.IPDomain) bool // If not nil, called to see if domain of requests matches. Host can be zero value for invalid domain/ip.
395 Path string // Path to register, like on http.ServeMux.
396 Handler http.Handler
397}
398
399type serve struct {
400 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https).
401 TLSConfig *tls.Config
402 NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port.
403 Favicon bool
404 Forwarded bool // Requests are coming from a reverse proxy, we'll use X-Forwarded-For for the IP address to ratelimit.
405 RateLimitDisabled bool // Don't apply ratelimiting.
406
407 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
408 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
409 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
410 // interfaces) last. WebHandlers can also pass requests to the internal servers.
411 // This order allows admins to serve other content on domains serving the mox.conf
412 // internal services.
413 SystemHandlers []pathHandler // Sorted, longest first.
414 Webserver bool
415 ServiceHandlers []pathHandler // Sorted, longest first.
416}
417
418// SystemHandle registers a named system handler for a path and optional host. If
419// path ends with a slash, it is used as prefix match, otherwise a full path match
420// is required. If hostOpt is set, only requests to those host are handled by this
421// handler.
422func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
423 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
424}
425
426// Like SystemHandle, but for internal services "admin", "account", "webmail",
427// "webapi" configured in the mox.conf Listener.
428func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
429 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
430}
431
432var (
433 limiterConnectionrate = &ratelimit.Limiter{
434 WindowLimits: []ratelimit.WindowLimit{
435 {
436 Window: time.Minute,
437 Limits: [...]int64{1000, 3000, 9000},
438 },
439 {
440 Window: time.Hour,
441 Limits: [...]int64{5000, 15000, 45000},
442 },
443 },
444 }
445)
446
447// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
448// right pathHandler or WebHandler, and it generates access logs and tracks
449// metrics.
450func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
451 now := time.Now()
452
453 // Rate limiting as early as possible, if enabled.
454 if !s.RateLimitDisabled {
455 // If requests are coming from a reverse proxy, use the IP from X-Forwarded-For.
456 // Otherwise the remote IP for this connection.
457 var ipstr string
458 if s.Forwarded {
459 s := r.Header.Get("X-Forwarded-For")
460 ipstr = strings.TrimSpace(strings.Split(s, ",")[0])
461 if ipstr == "" {
462 pkglog.Debug("ratelimit: no ip address in X-Forwarded-For header")
463 }
464 } else {
465 var err error
466 ipstr, _, err = net.SplitHostPort(r.RemoteAddr)
467 if err != nil {
468 pkglog.Debugx("ratelimit: parsing remote address", err, slog.String("remoteaddr", r.RemoteAddr))
469 }
470 }
471 ip := net.ParseIP(ipstr)
472 if ip == nil && ipstr != "" {
473 pkglog.Debug("ratelimit: invalid ip", slog.String("ip", ipstr))
474 }
475 if ip != nil && !limiterConnectionrate.Add(ip, now, 1) {
476 method := metricHTTPMethod(r.Method)
477 proto := "http"
478 if r.TLS != nil {
479 proto = "https"
480 }
481 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
482 // No logging, that's just noise.
483
484 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
485 return
486 }
487 }
488
489 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
490 r = r.WithContext(ctx)
491
492 wf, ok := xw.(responseWriterFlusher)
493 if !ok {
494 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
495 return
496 }
497
498 nw := &loggingWriter{
499 W: wf,
500 Start: now,
501 R: r,
502 Forwarded: s.Forwarded,
503 }
504 defer nw.Done()
505
506 // Cleanup path, removing ".." and ".". Keep any trailing slash.
507 trailingPath := strings.HasSuffix(r.URL.Path, "/")
508 if r.URL.Path == "" {
509 r.URL.Path = "/"
510 }
511 r.URL.Path = path.Clean(r.URL.Path)
512 if r.URL.Path == "." {
513 r.URL.Path = "/"
514 }
515 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
516 r.URL.Path += "/"
517 }
518
519 host := r.Host
520 nhost, _, err := net.SplitHostPort(host)
521 if err == nil {
522 host = nhost
523 }
524 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
525 if ipdom.IP == nil {
526 dom, domErr := dns.ParseDomain(host)
527 if domErr == nil {
528 ipdom = dns.IPDomain{Domain: dom}
529 }
530 }
531
532 handle := func(h pathHandler) bool {
533 if h.HostMatch != nil && !h.HostMatch(ipdom) {
534 return false
535 }
536 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
537 nw.Handler = h.Name
538 nw.Compress = true
539 h.Handler.ServeHTTP(nw, r)
540 return true
541 }
542 return false
543 }
544
545 for _, h := range s.SystemHandlers {
546 if handle(h) {
547 return
548 }
549 }
550 if s.Webserver {
551 if WebHandle(nw, r, ipdom) {
552 return
553 }
554 }
555 for _, h := range s.ServiceHandlers {
556 if handle(h) {
557 return
558 }
559 }
560 nw.Handler = "(nomatch)"
561 http.NotFound(nw, r)
562}
563
564func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
565 // Helpfully redirect user to version with ending slash.
566 if path != "/" && strings.HasSuffix(path, "/") {
567 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
568 http.Redirect(w, r, path, http.StatusSeeOther)
569 }))
570 srv.ServiceHandle(name, hostMatch, strings.TrimRight(path, "/"), handler)
571 }
572}
573
574// Listen binds to sockets for HTTP listeners, including those required for ACME to
575// generate TLS certificates. It stores the listeners so Serve can start serving them.
576func Listen() {
577 // Initialize listeners in deterministic order for the same potential error
578 // messages.
579 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
580 for _, name := range names {
581 l := mox.Conf.Static.Listeners[name]
582 portServe := portServes(name, l)
583
584 ports := slices.Sorted(maps.Keys(portServe))
585 for _, port := range ports {
586 srv := portServe[port]
587 for _, ip := range l.IPs {
588 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, srv.NextProto)
589 }
590 }
591 }
592}
593
594func portServes(name string, l config.Listener) map[int]*serve {
595 portServe := map[int]*serve{}
596
597 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
598 localhost := dns.Domain{ASCII: "localhost"}
599
600 ldom := l.HostnameDomain
601 if l.Hostname == "" {
602 ldom = mox.Conf.Static.HostnameDomain
603 }
604 listenerHostMatch := func(host dns.IPDomain) bool {
605 if host.IsIP() {
606 return true
607 }
608 return host.Domain == ldom || host.Domain == localhost
609 }
610 accountHostMatch := func(host dns.IPDomain) bool {
611 if listenerHostMatch(host) {
612 return true
613 }
614 return mox.Conf.IsClientSettingsDomain(host.Domain)
615 }
616
617 var ensureServe func(https, forwarded, noRateLimiting bool, port int, kind string, favicon bool) *serve
618 ensureServe = func(https, forwarded, rateLimitDisabled bool, port int, kind string, favicon bool) *serve {
619 s := portServe[port]
620 if s == nil {
621 s = &serve{nil, nil, tlsNextProtoMap{}, false, false, false, nil, false, nil}
622 portServe[port] = s
623 }
624 s.Kinds = append(s.Kinds, kind)
625 if favicon && !s.Favicon {
626 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
627 s.Favicon = true
628 }
629 s.Forwarded = s.Forwarded || forwarded
630 s.RateLimitDisabled = s.RateLimitDisabled || rateLimitDisabled
631
632 // We clone TLS configs because we may modify it later on for this server, for
633 // ALPN. And we need copies because multiple listeners on http.Server where the
634 // config is used will try to modify it concurrently.
635 if https && l.TLS.ACME != "" {
636 s.TLSConfig = l.TLS.ACMEConfig.Clone()
637
638 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
639 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
640 ensureServe(true, false, false, tlsport, "acme-tls-alpn-01", false)
641 }
642 } else if https {
643 s.TLSConfig = l.TLS.Config.Clone()
644 }
645 return s
646 }
647
648 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
649 // yet, add http-01 validation mechanism handler to server.
650 ensureACMEHTTP01 := func(srv *serve) {
651 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
652 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
653 srv.Kinds = append(srv.Kinds, "acme-http-01")
654 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
655 }
656 }
657
658 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
659 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
660 ensureServe(true, false, false, port, "acme-tls-alpn-01", false)
661 }
662 if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
663 s := ensureServe(true, false, false, 443, "smtp-https", false)
664 hostname := mox.Conf.Static.HostnameDomain
665 if l.Hostname != "" {
666 hostname = l.HostnameDomain
667 }
668
669 maxMsgSize := l.SMTPMaxMessageSize
670 if maxMsgSize == 0 {
671 maxMsgSize = config.DefaultMaxMsgSize
672 }
673 requireTLS := !l.SMTP.NoRequireTLS
674
675 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
676 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
677 }
678 }
679 if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
680 s := ensureServe(true, false, false, 443, "imap-https", false)
681 s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
682 imapserver.ServeTLSConn(name, conn, s.TLSConfig)
683 }
684 }
685 if l.AccountHTTP.Enabled {
686 port := config.Port(l.AccountHTTP.Port, 80)
687 path := "/"
688 if l.AccountHTTP.Path != "" {
689 path = l.AccountHTTP.Path
690 }
691 srv := ensureServe(false, l.AccountHTTP.Forwarded, false, port, "account-http at "+path, true)
692 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
693 srv.ServiceHandle("account", accountHostMatch, path, handler)
694 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
695 ensureACMEHTTP01(srv)
696 }
697 if l.AccountHTTPS.Enabled {
698 port := config.Port(l.AccountHTTPS.Port, 443)
699 path := "/"
700 if l.AccountHTTPS.Path != "" {
701 path = l.AccountHTTPS.Path
702 }
703 srv := ensureServe(true, l.AccountHTTPS.Forwarded, false, port, "account-https at "+path, true)
704 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
705 srv.ServiceHandle("account", accountHostMatch, path, handler)
706 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
707 }
708
709 if l.AdminHTTP.Enabled {
710 port := config.Port(l.AdminHTTP.Port, 80)
711 path := "/admin/"
712 if l.AdminHTTP.Path != "" {
713 path = l.AdminHTTP.Path
714 }
715 srv := ensureServe(false, l.AdminHTTP.Forwarded, false, port, "admin-http at "+path, true)
716 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
717 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
718 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
719 ensureACMEHTTP01(srv)
720 }
721 if l.AdminHTTPS.Enabled {
722 port := config.Port(l.AdminHTTPS.Port, 443)
723 path := "/admin/"
724 if l.AdminHTTPS.Path != "" {
725 path = l.AdminHTTPS.Path
726 }
727 srv := ensureServe(true, l.AdminHTTPS.Forwarded, false, port, "admin-https at "+path, true)
728 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
729 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
730 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
731 }
732
733 maxMsgSize := l.SMTPMaxMessageSize
734 if maxMsgSize == 0 {
735 maxMsgSize = config.DefaultMaxMsgSize
736 }
737
738 if l.WebAPIHTTP.Enabled {
739 port := config.Port(l.WebAPIHTTP.Port, 80)
740 path := "/webapi/"
741 if l.WebAPIHTTP.Path != "" {
742 path = l.WebAPIHTTP.Path
743 }
744 srv := ensureServe(false, l.WebAPIHTTP.Forwarded, false, port, "webapi-http at "+path, true)
745 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
746 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
747 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
748 ensureACMEHTTP01(srv)
749 }
750 if l.WebAPIHTTPS.Enabled {
751 port := config.Port(l.WebAPIHTTPS.Port, 443)
752 path := "/webapi/"
753 if l.WebAPIHTTPS.Path != "" {
754 path = l.WebAPIHTTPS.Path
755 }
756 srv := ensureServe(true, l.WebAPIHTTPS.Forwarded, false, port, "webapi-https at "+path, true)
757 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
758 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
759 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
760 }
761
762 if l.WebmailHTTP.Enabled {
763 port := config.Port(l.WebmailHTTP.Port, 80)
764 path := "/webmail/"
765 if l.WebmailHTTP.Path != "" {
766 path = l.WebmailHTTP.Path
767 }
768 srv := ensureServe(false, l.WebmailHTTP.Forwarded, false, port, "webmail-http at "+path, true)
769 var accountPath string
770 if l.AccountHTTP.Enabled {
771 accountPath = "/"
772 if l.AccountHTTP.Path != "" {
773 accountPath = l.AccountHTTP.Path
774 }
775 }
776 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
777 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
778 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
779 ensureACMEHTTP01(srv)
780 }
781 if l.WebmailHTTPS.Enabled {
782 port := config.Port(l.WebmailHTTPS.Port, 443)
783 path := "/webmail/"
784 if l.WebmailHTTPS.Path != "" {
785 path = l.WebmailHTTPS.Path
786 }
787 srv := ensureServe(true, l.WebmailHTTPS.Forwarded, false, port, "webmail-https at "+path, true)
788 var accountPath string
789 if l.AccountHTTPS.Enabled {
790 accountPath = "/"
791 if l.AccountHTTPS.Path != "" {
792 accountPath = l.AccountHTTPS.Path
793 }
794 }
795 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
796 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
797 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
798 }
799
800 if l.MetricsHTTP.Enabled {
801 port := config.Port(l.MetricsHTTP.Port, 8010)
802 srv := ensureServe(false, false, false, port, "metrics-http", false)
803 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
804 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
805 if r.URL.Path != "/" {
806 http.NotFound(w, r)
807 return
808 } else if r.Method != "GET" {
809 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
810 return
811 }
812 w.Header().Set("Content-Type", "text/html")
813 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
814 })))
815 }
816 if l.AutoconfigHTTPS.Enabled {
817 port := config.Port(l.AutoconfigHTTPS.Port, 443)
818 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, l.AutoconfigHTTPS.Forwarded, false, port, "autoconfig-https", false)
819 if l.AutoconfigHTTPS.NonTLS {
820 ensureACMEHTTP01(srv)
821 }
822 autoconfigMatch := func(ipdom dns.IPDomain) bool {
823 dom := ipdom.Domain
824 if dom.IsZero() {
825 return false
826 }
827 // Thunderbird requests an autodiscovery URL at the email address domain name, so
828 // autoconfig prefix is optional.
829 if after, ok := strings.CutPrefix(dom.ASCII, "autoconfig."); ok {
830 dom.ASCII = after
831 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
832 }
833 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
834 // use the mail server's host name.
835 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
836 return true
837 }
838 dc, ok := mox.Conf.Domain(dom)
839 return ok && !dc.ReportsOnly
840 }
841 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
842 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
843 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
844 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
845 }
846 if l.MTASTSHTTPS.Enabled {
847 port := config.Port(l.MTASTSHTTPS.Port, 443)
848 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, l.MTASTSHTTPS.Forwarded, false, port, "mtasts-https", false)
849 if l.MTASTSHTTPS.NonTLS {
850 ensureACMEHTTP01(srv)
851 }
852 mtastsMatch := func(ipdom dns.IPDomain) bool {
853 // todo: may want to check this against the configured domains, could in theory be just a webserver.
854 dom := ipdom.Domain
855 if dom.IsZero() {
856 return false
857 }
858 return strings.HasPrefix(dom.ASCII, "mta-sts.")
859 }
860 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
861 }
862 if l.PprofHTTP.Enabled {
863 // Importing net/http/pprof registers handlers on the default serve mux.
864 port := config.Port(l.PprofHTTP.Port, 8011)
865 if _, ok := portServe[port]; ok {
866 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
867 }
868 srv := &serve{[]string{"pprof-http"}, nil, nil, false, false, false, nil, false, nil}
869 portServe[port] = srv
870 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
871 }
872 if l.WebserverHTTP.Enabled {
873 port := config.Port(l.WebserverHTTP.Port, 80)
874 srv := ensureServe(false, false, l.WebserverHTTP.RateLimitDisabled, port, "webserver-http", false)
875 srv.Webserver = true
876 ensureACMEHTTP01(srv)
877 }
878 if l.WebserverHTTPS.Enabled {
879 port := config.Port(l.WebserverHTTPS.Port, 443)
880 srv := ensureServe(true, false, l.WebserverHTTPS.RateLimitDisabled, port, "webserver-https", false)
881 srv.Webserver = true
882 }
883
884 if l.TLS != nil && l.TLS.ACME != "" {
885 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
886 if ensureManagerHosts[m] == nil {
887 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
888 }
889 hosts := ensureManagerHosts[m]
890 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
891
892 if l.HostnameDomain.ASCII != "" {
893 hosts[l.HostnameDomain] = struct{}{}
894 }
895
896 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
897 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
898 // for the timeouts of clients doing autoconfig.
899
900 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
901 for _, name := range mox.Conf.Domains() {
902 if dom, err := dns.ParseDomain(name); err != nil {
903 pkglog.Errorx("parsing domain from config", err)
904 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly || d.Disabled {
905 // Do not gather autoconfig name if we aren't accepting email for this domain or when it is disabled.
906 continue
907 }
908
909 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
910 if err != nil {
911 pkglog.Errorx("parsing domain from config for autoconfig", err)
912 } else {
913 hosts[autoconfdom] = struct{}{}
914 }
915 }
916 }
917 }
918
919 if s := portServe[443]; s != nil && s.TLSConfig != nil && len(s.NextProto) > 0 {
920 s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, slices.Collect(maps.Keys(s.NextProto))...)
921 }
922
923 for _, srv := range portServe {
924 sortPathHandlers(srv.SystemHandlers)
925 sortPathHandlers(srv.ServiceHandlers)
926 }
927
928 return portServe
929}
930
931func sortPathHandlers(l []pathHandler) {
932 sort.Slice(l, func(i, j int) bool {
933 a := l[i].Path
934 b := l[j].Path
935 if len(a) == len(b) {
936 // For consistent order.
937 return a < b
938 }
939 // Longest paths first.
940 return len(a) > len(b)
941 })
942}
943
944// functions to be launched in goroutine that will serve on a listener.
945var servers []func()
946
947// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
948// immediately after startup. We only do so for our explicit listener hostnames,
949// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
950// do request autoconfig, otherwise clients may run into their timeouts waiting for
951// the certificate to be given during the first https connection.
952var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
953
954type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
955
956// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
957func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, nextProto tlsNextProtoMap) {
958 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
959
960 var protocol string
961 var ln net.Listener
962 var err error
963 if tlsConfig == nil {
964 protocol = "http"
965 if os.Getuid() == 0 {
966 pkglog.Print("http listener",
967 slog.String("name", name),
968 slog.String("kinds", strings.Join(kinds, ",")),
969 slog.String("address", addr))
970 }
971 ln, err = mox.Listen(mox.Network(ip), addr)
972 if err != nil {
973 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
974 }
975 } else {
976 protocol = "https"
977 if os.Getuid() == 0 {
978 pkglog.Print("https listener",
979 slog.String("name", name),
980 slog.String("kinds", strings.Join(kinds, ",")),
981 slog.String("address", addr))
982 }
983 ln, err = mox.Listen(mox.Network(ip), addr)
984 if err != nil {
985 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
986 }
987 ln = tls.NewListener(ln, tlsConfig)
988 }
989
990 server := &http.Server{
991 Handler: handler,
992 TLSConfig: tlsConfig,
993 ReadHeaderTimeout: 30 * time.Second,
994 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
995 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
996 TLSNextProto: nextProto,
997 }
998 // By default, the Go 1.6 and above http.Server includes support for HTTP2.
999 // However, HTTP2 is negotiated via ALPN. Because we are configuring
1000 // TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
1001 // and calling ConfigureServer.
1002 err = http2.ConfigureServer(server, nil)
1003 if err != nil {
1004 pkglog.Fatalx("https: unable to configure http2", err)
1005 }
1006 serve := func() {
1007 err := server.Serve(ln)
1008 pkglog.Fatalx(protocol+": serve", err)
1009 }
1010 servers = append(servers, serve)
1011}
1012
1013// Serve starts serving on the initialized listeners.
1014func Serve() {
1015 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
1016
1017 go webaccount.ImportManage()
1018
1019 for _, serve := range servers {
1020 go serve()
1021 }
1022 servers = nil
1023
1024 go func() {
1025 time.Sleep(1 * time.Second)
1026 i := 0
1027 for m, hosts := range ensureManagerHosts {
1028 for host := range hosts {
1029 // Check if certificate is already available. If so, we don't print as much after a
1030 // restart, and finish more quickly if only a few certificates are missing/old.
1031 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
1032 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
1033 } else if avail {
1034 continue
1035 }
1036
1037 if i >= 10 {
1038 // Just in case someone adds quite some domains to their config. We don't want to
1039 // hit any ACME rate limits.
1040 return
1041 }
1042 if i > 0 {
1043 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
1044 time.Sleep(10 * time.Second)
1045 }
1046 i++
1047
1048 hello := &tls.ClientHelloInfo{
1049 ServerName: host.ASCII,
1050
1051 // Make us fetch an ECDSA P256 cert.
1052 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
1053 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
1054 SupportedCurves: []tls.CurveID{tls.CurveP256},
1055 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
1056 SupportedVersions: []uint16{tls.VersionTLS13},
1057 }
1058 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
1059 if _, err := m.Manager.GetCertificate(hello); err != nil {
1060 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))
1061 }
1062 }
1063 }
1064 }()
1065}
1066