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