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