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