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("referer", w.R.Header.Get("Referer")),
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}
389
390type serve struct {
391 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https).
392 TLSConfig *tls.Config
393 NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port.
394 Favicon bool
395 Forwarded bool // Requests are coming from a reverse proxy, we'll use X-Forwarded-For for the IP address to ratelimit.
396 RateLimitDisabled bool // Don't apply ratelimiting.
397
398 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
399 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
400 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
401 // interfaces) last. WebHandlers can also pass requests to the internal servers.
402 // This order allows admins to serve other content on domains serving the mox.conf
403 // internal services.
404 SystemHandlers []pathHandler // Sorted, longest first.
405 Webserver bool
406 ServiceHandlers []pathHandler // Sorted, longest first.
407}
408
409// SystemHandle registers a named system handler for a path and optional host. If
410// path ends with a slash, it is used as prefix match, otherwise a full path match
411// is required. If hostOpt is set, only requests to those host are handled by this
412// handler.
413func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
414 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
415}
416
417// Like SystemHandle, but for internal services "admin", "account", "webmail",
418// "webapi" configured in the mox.conf Listener.
419func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
420 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
421}
422
423var (
424 limiterConnectionrate = &ratelimit.Limiter{
425 WindowLimits: []ratelimit.WindowLimit{
426 {
427 Window: time.Minute,
428 Limits: [...]int64{1000, 3000, 9000},
429 },
430 {
431 Window: time.Hour,
432 Limits: [...]int64{5000, 15000, 45000},
433 },
434 },
435 }
436)
437
438// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
439// right pathHandler or WebHandler, and it generates access logs and tracks
440// metrics.
441func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
442 now := time.Now()
443
444 // Rate limiting as early as possible, if enabled.
445 if !s.RateLimitDisabled {
446 // If requests are coming from a reverse proxy, use the IP from X-Forwarded-For.
447 // Otherwise the remote IP for this connection.
448 var ipstr string
449 if s.Forwarded {
450 s := r.Header.Get("X-Forwarded-For")
451 ipstr = strings.TrimSpace(strings.Split(s, ",")[0])
452 if ipstr == "" {
453 pkglog.Debug("ratelimit: no ip address in X-Forwarded-For header")
454 }
455 } else {
456 var err error
457 ipstr, _, err = net.SplitHostPort(r.RemoteAddr)
458 if err != nil {
459 pkglog.Debugx("ratelimit: parsing remote address", err, slog.String("remoteaddr", r.RemoteAddr))
460 }
461 }
462 ip := net.ParseIP(ipstr)
463 if ip == nil && ipstr != "" {
464 pkglog.Debug("ratelimit: invalid ip", slog.String("ip", ipstr))
465 }
466 if ip != nil && !limiterConnectionrate.Add(ip, now, 1) {
467 method := metricHTTPMethod(r.Method)
468 proto := "http"
469 if r.TLS != nil {
470 proto = "https"
471 }
472 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
473 // No logging, that's just noise.
474
475 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
476 return
477 }
478 }
479
480 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
481 r = r.WithContext(ctx)
482
483 wf, ok := xw.(responseWriterFlusher)
484 if !ok {
485 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
486 return
487 }
488
489 nw := &loggingWriter{
490 W: wf,
491 Start: now,
492 R: r,
493 }
494 defer nw.Done()
495
496 // Cleanup path, removing ".." and ".". Keep any trailing slash.
497 trailingPath := strings.HasSuffix(r.URL.Path, "/")
498 if r.URL.Path == "" {
499 r.URL.Path = "/"
500 }
501 r.URL.Path = path.Clean(r.URL.Path)
502 if r.URL.Path == "." {
503 r.URL.Path = "/"
504 }
505 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
506 r.URL.Path += "/"
507 }
508
509 host := r.Host
510 nhost, _, err := net.SplitHostPort(host)
511 if err == nil {
512 host = nhost
513 }
514 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
515 if ipdom.IP == nil {
516 dom, domErr := dns.ParseDomain(host)
517 if domErr == nil {
518 ipdom = dns.IPDomain{Domain: dom}
519 }
520 }
521
522 handle := func(h pathHandler) bool {
523 if h.HostMatch != nil && !h.HostMatch(ipdom) {
524 return false
525 }
526 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
527 nw.Handler = h.Name
528 nw.Compress = true
529 h.Handler.ServeHTTP(nw, r)
530 return true
531 }
532 return false
533 }
534
535 for _, h := range s.SystemHandlers {
536 if handle(h) {
537 return
538 }
539 }
540 if s.Webserver {
541 if WebHandle(nw, r, ipdom) {
542 return
543 }
544 }
545 for _, h := range s.ServiceHandlers {
546 if handle(h) {
547 return
548 }
549 }
550 nw.Handler = "(nomatch)"
551 http.NotFound(nw, r)
552}
553
554func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
555 // Helpfully redirect user to version with ending slash.
556 if path != "/" && strings.HasSuffix(path, "/") {
557 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
558 http.Redirect(w, r, path, http.StatusSeeOther)
559 }))
560 srv.ServiceHandle(name, hostMatch, strings.TrimRight(path, "/"), handler)
561 }
562}
563
564// Listen binds to sockets for HTTP listeners, including those required for ACME to
565// generate TLS certificates. It stores the listeners so Serve can start serving them.
566func Listen() {
567 // Initialize listeners in deterministic order for the same potential error
568 // messages.
569 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
570 for _, name := range names {
571 l := mox.Conf.Static.Listeners[name]
572 portServe := portServes(name, l)
573
574 ports := slices.Sorted(maps.Keys(portServe))
575 for _, port := range ports {
576 srv := portServe[port]
577 for _, ip := range l.IPs {
578 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, srv.NextProto)
579 }
580 }
581 }
582}
583
584func portServes(name string, l config.Listener) map[int]*serve {
585 portServe := map[int]*serve{}
586
587 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
588 localhost := dns.Domain{ASCII: "localhost"}
589
590 ldom := l.HostnameDomain
591 if l.Hostname == "" {
592 ldom = mox.Conf.Static.HostnameDomain
593 }
594 listenerHostMatch := func(host dns.IPDomain) bool {
595 if host.IsIP() {
596 return true
597 }
598 return host.Domain == ldom || host.Domain == localhost
599 }
600 accountHostMatch := func(host dns.IPDomain) bool {
601 if listenerHostMatch(host) {
602 return true
603 }
604 return mox.Conf.IsClientSettingsDomain(host.Domain)
605 }
606
607 var ensureServe func(https, forwarded, noRateLimiting bool, port int, kind string, favicon bool) *serve
608 ensureServe = func(https, forwarded, rateLimitDisabled bool, port int, kind string, favicon bool) *serve {
609 s := portServe[port]
610 if s == nil {
611 s = &serve{nil, nil, tlsNextProtoMap{}, false, false, false, nil, false, nil}
612 portServe[port] = s
613 }
614 s.Kinds = append(s.Kinds, kind)
615 if favicon && !s.Favicon {
616 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
617 s.Favicon = true
618 }
619 s.Forwarded = s.Forwarded || forwarded
620 s.RateLimitDisabled = s.RateLimitDisabled || rateLimitDisabled
621
622 // We clone TLS configs because we may modify it later on for this server, for
623 // ALPN. And we need copies because multiple listeners on http.Server where the
624 // config is used will try to modify it concurrently.
625 if https && l.TLS.ACME != "" {
626 s.TLSConfig = l.TLS.ACMEConfig.Clone()
627
628 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
629 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
630 ensureServe(true, false, false, tlsport, "acme-tls-alpn-01", false)
631 }
632 } else if https {
633 s.TLSConfig = l.TLS.Config.Clone()
634 }
635 return s
636 }
637
638 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
639 // yet, add http-01 validation mechanism handler to server.
640 ensureACMEHTTP01 := func(srv *serve) {
641 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
642 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
643 srv.Kinds = append(srv.Kinds, "acme-http-01")
644 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
645 }
646 }
647
648 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
649 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
650 ensureServe(true, false, false, port, "acme-tls-alpn-01", false)
651 }
652 if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
653 s := ensureServe(true, false, false, 443, "smtp-https", false)
654 hostname := mox.Conf.Static.HostnameDomain
655 if l.Hostname != "" {
656 hostname = l.HostnameDomain
657 }
658
659 maxMsgSize := l.SMTPMaxMessageSize
660 if maxMsgSize == 0 {
661 maxMsgSize = config.DefaultMaxMsgSize
662 }
663 requireTLS := !l.SMTP.NoRequireTLS
664
665 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
666 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
667 }
668 }
669 if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
670 s := ensureServe(true, false, false, 443, "imap-https", false)
671 s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
672 imapserver.ServeTLSConn(name, conn, s.TLSConfig)
673 }
674 }
675 if l.AccountHTTP.Enabled {
676 port := config.Port(l.AccountHTTP.Port, 80)
677 path := "/"
678 if l.AccountHTTP.Path != "" {
679 path = l.AccountHTTP.Path
680 }
681 srv := ensureServe(false, l.AccountHTTP.Forwarded, false, port, "account-http at "+path, true)
682 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
683 srv.ServiceHandle("account", accountHostMatch, path, handler)
684 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
685 ensureACMEHTTP01(srv)
686 }
687 if l.AccountHTTPS.Enabled {
688 port := config.Port(l.AccountHTTPS.Port, 443)
689 path := "/"
690 if l.AccountHTTPS.Path != "" {
691 path = l.AccountHTTPS.Path
692 }
693 srv := ensureServe(true, l.AccountHTTPS.Forwarded, false, port, "account-https at "+path, true)
694 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
695 srv.ServiceHandle("account", accountHostMatch, path, handler)
696 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
697 }
698
699 if l.AdminHTTP.Enabled {
700 port := config.Port(l.AdminHTTP.Port, 80)
701 path := "/admin/"
702 if l.AdminHTTP.Path != "" {
703 path = l.AdminHTTP.Path
704 }
705 srv := ensureServe(false, l.AdminHTTP.Forwarded, false, port, "admin-http at "+path, true)
706 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
707 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
708 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
709 ensureACMEHTTP01(srv)
710 }
711 if l.AdminHTTPS.Enabled {
712 port := config.Port(l.AdminHTTPS.Port, 443)
713 path := "/admin/"
714 if l.AdminHTTPS.Path != "" {
715 path = l.AdminHTTPS.Path
716 }
717 srv := ensureServe(true, l.AdminHTTPS.Forwarded, false, port, "admin-https at "+path, true)
718 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
719 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
720 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
721 }
722
723 maxMsgSize := l.SMTPMaxMessageSize
724 if maxMsgSize == 0 {
725 maxMsgSize = config.DefaultMaxMsgSize
726 }
727
728 if l.WebAPIHTTP.Enabled {
729 port := config.Port(l.WebAPIHTTP.Port, 80)
730 path := "/webapi/"
731 if l.WebAPIHTTP.Path != "" {
732 path = l.WebAPIHTTP.Path
733 }
734 srv := ensureServe(false, l.WebAPIHTTP.Forwarded, false, port, "webapi-http at "+path, true)
735 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
736 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
737 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
738 ensureACMEHTTP01(srv)
739 }
740 if l.WebAPIHTTPS.Enabled {
741 port := config.Port(l.WebAPIHTTPS.Port, 443)
742 path := "/webapi/"
743 if l.WebAPIHTTPS.Path != "" {
744 path = l.WebAPIHTTPS.Path
745 }
746 srv := ensureServe(true, l.WebAPIHTTPS.Forwarded, false, port, "webapi-https at "+path, true)
747 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
748 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
749 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
750 }
751
752 if l.WebmailHTTP.Enabled {
753 port := config.Port(l.WebmailHTTP.Port, 80)
754 path := "/webmail/"
755 if l.WebmailHTTP.Path != "" {
756 path = l.WebmailHTTP.Path
757 }
758 srv := ensureServe(false, l.WebmailHTTP.Forwarded, false, port, "webmail-http at "+path, true)
759 var accountPath string
760 if l.AccountHTTP.Enabled {
761 accountPath = "/"
762 if l.AccountHTTP.Path != "" {
763 accountPath = l.AccountHTTP.Path
764 }
765 }
766 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
767 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
768 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
769 ensureACMEHTTP01(srv)
770 }
771 if l.WebmailHTTPS.Enabled {
772 port := config.Port(l.WebmailHTTPS.Port, 443)
773 path := "/webmail/"
774 if l.WebmailHTTPS.Path != "" {
775 path = l.WebmailHTTPS.Path
776 }
777 srv := ensureServe(true, l.WebmailHTTPS.Forwarded, false, port, "webmail-https at "+path, true)
778 var accountPath string
779 if l.AccountHTTPS.Enabled {
780 accountPath = "/"
781 if l.AccountHTTPS.Path != "" {
782 accountPath = l.AccountHTTPS.Path
783 }
784 }
785 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
786 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
787 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
788 }
789
790 if l.MetricsHTTP.Enabled {
791 port := config.Port(l.MetricsHTTP.Port, 8010)
792 srv := ensureServe(false, false, false, port, "metrics-http", false)
793 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
794 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
795 if r.URL.Path != "/" {
796 http.NotFound(w, r)
797 return
798 } else if r.Method != "GET" {
799 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
800 return
801 }
802 w.Header().Set("Content-Type", "text/html")
803 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
804 })))
805 }
806 if l.AutoconfigHTTPS.Enabled {
807 port := config.Port(l.AutoconfigHTTPS.Port, 443)
808 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, false, false, port, "autoconfig-https", false)
809 if l.AutoconfigHTTPS.NonTLS {
810 ensureACMEHTTP01(srv)
811 }
812 autoconfigMatch := func(ipdom dns.IPDomain) bool {
813 dom := ipdom.Domain
814 if dom.IsZero() {
815 return false
816 }
817 // Thunderbird requests an autodiscovery URL at the email address domain name, so
818 // autoconfig prefix is optional.
819 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
820 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
821 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
822 }
823 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
824 // use the mail server's host name.
825 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
826 return true
827 }
828 dc, ok := mox.Conf.Domain(dom)
829 return ok && !dc.ReportsOnly
830 }
831 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
832 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
833 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
834 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
835 }
836 if l.MTASTSHTTPS.Enabled {
837 port := config.Port(l.MTASTSHTTPS.Port, 443)
838 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, false, false, port, "mtasts-https", false)
839 if l.MTASTSHTTPS.NonTLS {
840 ensureACMEHTTP01(srv)
841 }
842 mtastsMatch := func(ipdom dns.IPDomain) bool {
843 // todo: may want to check this against the configured domains, could in theory be just a webserver.
844 dom := ipdom.Domain
845 if dom.IsZero() {
846 return false
847 }
848 return strings.HasPrefix(dom.ASCII, "mta-sts.")
849 }
850 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
851 }
852 if l.PprofHTTP.Enabled {
853 // Importing net/http/pprof registers handlers on the default serve mux.
854 port := config.Port(l.PprofHTTP.Port, 8011)
855 if _, ok := portServe[port]; ok {
856 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
857 }
858 srv := &serve{[]string{"pprof-http"}, nil, nil, false, false, false, nil, false, nil}
859 portServe[port] = srv
860 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
861 }
862 if l.WebserverHTTP.Enabled {
863 port := config.Port(l.WebserverHTTP.Port, 80)
864 srv := ensureServe(false, false, l.WebserverHTTP.RateLimitDisabled, port, "webserver-http", false)
865 srv.Webserver = true
866 ensureACMEHTTP01(srv)
867 }
868 if l.WebserverHTTPS.Enabled {
869 port := config.Port(l.WebserverHTTPS.Port, 443)
870 srv := ensureServe(true, false, l.WebserverHTTPS.RateLimitDisabled, port, "webserver-https", false)
871 srv.Webserver = true
872 }
873
874 if l.TLS != nil && l.TLS.ACME != "" {
875 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
876 if ensureManagerHosts[m] == nil {
877 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
878 }
879 hosts := ensureManagerHosts[m]
880 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
881
882 if l.HostnameDomain.ASCII != "" {
883 hosts[l.HostnameDomain] = struct{}{}
884 }
885
886 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
887 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
888 // for the timeouts of clients doing autoconfig.
889
890 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
891 for _, name := range mox.Conf.Domains() {
892 if dom, err := dns.ParseDomain(name); err != nil {
893 pkglog.Errorx("parsing domain from config", err)
894 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly || d.Disabled {
895 // Do not gather autoconfig name if we aren't accepting email for this domain or when it is disabled.
896 continue
897 }
898
899 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
900 if err != nil {
901 pkglog.Errorx("parsing domain from config for autoconfig", err)
902 } else {
903 hosts[autoconfdom] = struct{}{}
904 }
905 }
906 }
907 }
908
909 if s := portServe[443]; s != nil && s.TLSConfig != nil && len(s.NextProto) > 0 {
910 s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, slices.Collect(maps.Keys(s.NextProto))...)
911 }
912
913 for _, srv := range portServe {
914 sortPathHandlers(srv.SystemHandlers)
915 sortPathHandlers(srv.ServiceHandlers)
916 }
917
918 return portServe
919}
920
921func sortPathHandlers(l []pathHandler) {
922 sort.Slice(l, func(i, j int) bool {
923 a := l[i].Path
924 b := l[j].Path
925 if len(a) == len(b) {
926 // For consistent order.
927 return a < b
928 }
929 // Longest paths first.
930 return len(a) > len(b)
931 })
932}
933
934// functions to be launched in goroutine that will serve on a listener.
935var servers []func()
936
937// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
938// immediately after startup. We only do so for our explicit listener hostnames,
939// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
940// do request autoconfig, otherwise clients may run into their timeouts waiting for
941// the certificate to be given during the first https connection.
942var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
943
944type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
945
946// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
947func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, nextProto tlsNextProtoMap) {
948 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
949
950 var protocol string
951 var ln net.Listener
952 var err error
953 if tlsConfig == nil {
954 protocol = "http"
955 if os.Getuid() == 0 {
956 pkglog.Print("http listener",
957 slog.String("name", name),
958 slog.String("kinds", strings.Join(kinds, ",")),
959 slog.String("address", addr))
960 }
961 ln, err = mox.Listen(mox.Network(ip), addr)
962 if err != nil {
963 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
964 }
965 } else {
966 protocol = "https"
967 if os.Getuid() == 0 {
968 pkglog.Print("https listener",
969 slog.String("name", name),
970 slog.String("kinds", strings.Join(kinds, ",")),
971 slog.String("address", addr))
972 }
973 ln, err = mox.Listen(mox.Network(ip), addr)
974 if err != nil {
975 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
976 }
977 ln = tls.NewListener(ln, tlsConfig)
978 }
979
980 server := &http.Server{
981 Handler: handler,
982 TLSConfig: tlsConfig,
983 ReadHeaderTimeout: 30 * time.Second,
984 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
985 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
986 TLSNextProto: nextProto,
987 }
988 // By default, the Go 1.6 and above http.Server includes support for HTTP2.
989 // However, HTTP2 is negotiated via ALPN. Because we are configuring
990 // TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
991 // and calling ConfigureServer.
992 err = http2.ConfigureServer(server, nil)
993 if err != nil {
994 pkglog.Fatalx("https: unable to configure http2", err)
995 }
996 serve := func() {
997 err := server.Serve(ln)
998 pkglog.Fatalx(protocol+": serve", err)
999 }
1000 servers = append(servers, serve)
1001}
1002
1003// Serve starts serving on the initialized listeners.
1004func Serve() {
1005 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
1006
1007 go webaccount.ImportManage()
1008
1009 for _, serve := range servers {
1010 go serve()
1011 }
1012 servers = nil
1013
1014 go func() {
1015 time.Sleep(1 * time.Second)
1016 i := 0
1017 for m, hosts := range ensureManagerHosts {
1018 for host := range hosts {
1019 // Check if certificate is already available. If so, we don't print as much after a
1020 // restart, and finish more quickly if only a few certificates are missing/old.
1021 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
1022 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
1023 } else if avail {
1024 continue
1025 }
1026
1027 if i >= 10 {
1028 // Just in case someone adds quite some domains to their config. We don't want to
1029 // hit any ACME rate limits.
1030 return
1031 }
1032 if i > 0 {
1033 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
1034 time.Sleep(10 * time.Second)
1035 }
1036 i++
1037
1038 hello := &tls.ClientHelloInfo{
1039 ServerName: host.ASCII,
1040
1041 // Make us fetch an ECDSA P256 cert.
1042 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
1043 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
1044 SupportedCurves: []tls.CurveID{tls.CurveP256},
1045 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
1046 SupportedVersions: []uint16{tls.VersionTLS13},
1047 }
1048 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
1049 if _, err := m.Manager.GetCertificate(hello); err != nil {
1050 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))
1051 }
1052 }
1053 }
1054 }()
1055}
1056