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 } else if https {
600 s.TLSConfig = l.TLS.Config
601 if l.TLS.ACME != "" {
602 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
603 ensureServe(true, tlsport, "acme-tls-alpn-01", false)
604 }
605 }
606 return s
607 }
608
609 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
610 // yet, add http-01 validation mechanism handler to server.
611 ensureACMEHTTP01 := func(srv *serve) {
612 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
613 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
614 srv.Kinds = append(srv.Kinds, "acme-http-01")
615 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
616 }
617 }
618
619 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
620 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
621 ensureServe(true, port, "acme-tls-alpn-01", false)
622 }
623
624 if l.AccountHTTP.Enabled {
625 port := config.Port(l.AccountHTTP.Port, 80)
626 path := "/"
627 if l.AccountHTTP.Path != "" {
628 path = l.AccountHTTP.Path
629 }
630 srv := ensureServe(false, port, "account-http at "+path, true)
631 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
632 srv.ServiceHandle("account", accountHostMatch, path, handler)
633 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
634 ensureACMEHTTP01(srv)
635 }
636 if l.AccountHTTPS.Enabled {
637 port := config.Port(l.AccountHTTPS.Port, 443)
638 path := "/"
639 if l.AccountHTTPS.Path != "" {
640 path = l.AccountHTTPS.Path
641 }
642 srv := ensureServe(true, port, "account-https at "+path, true)
643 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
644 srv.ServiceHandle("account", accountHostMatch, path, handler)
645 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
646 }
647
648 if l.AdminHTTP.Enabled {
649 port := config.Port(l.AdminHTTP.Port, 80)
650 path := "/admin/"
651 if l.AdminHTTP.Path != "" {
652 path = l.AdminHTTP.Path
653 }
654 srv := ensureServe(false, port, "admin-http at "+path, true)
655 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
656 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
657 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
658 ensureACMEHTTP01(srv)
659 }
660 if l.AdminHTTPS.Enabled {
661 port := config.Port(l.AdminHTTPS.Port, 443)
662 path := "/admin/"
663 if l.AdminHTTPS.Path != "" {
664 path = l.AdminHTTPS.Path
665 }
666 srv := ensureServe(true, port, "admin-https at "+path, true)
667 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
668 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
669 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
670 }
671
672 maxMsgSize := l.SMTPMaxMessageSize
673 if maxMsgSize == 0 {
674 maxMsgSize = config.DefaultMaxMsgSize
675 }
676
677 if l.WebAPIHTTP.Enabled {
678 port := config.Port(l.WebAPIHTTP.Port, 80)
679 path := "/webapi/"
680 if l.WebAPIHTTP.Path != "" {
681 path = l.WebAPIHTTP.Path
682 }
683 srv := ensureServe(false, port, "webapi-http at "+path, true)
684 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
685 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
686 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
687 ensureACMEHTTP01(srv)
688 }
689 if l.WebAPIHTTPS.Enabled {
690 port := config.Port(l.WebAPIHTTPS.Port, 443)
691 path := "/webapi/"
692 if l.WebAPIHTTPS.Path != "" {
693 path = l.WebAPIHTTPS.Path
694 }
695 srv := ensureServe(true, port, "webapi-https at "+path, true)
696 handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
697 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
698 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
699 }
700
701 if l.WebmailHTTP.Enabled {
702 port := config.Port(l.WebmailHTTP.Port, 80)
703 path := "/webmail/"
704 if l.WebmailHTTP.Path != "" {
705 path = l.WebmailHTTP.Path
706 }
707 srv := ensureServe(false, port, "webmail-http at "+path, true)
708 var accountPath string
709 if l.AccountHTTP.Enabled {
710 accountPath = "/"
711 if l.AccountHTTP.Path != "" {
712 accountPath = l.AccountHTTP.Path
713 }
714 }
715 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
716 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
717 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
718 ensureACMEHTTP01(srv)
719 }
720 if l.WebmailHTTPS.Enabled {
721 port := config.Port(l.WebmailHTTPS.Port, 443)
722 path := "/webmail/"
723 if l.WebmailHTTPS.Path != "" {
724 path = l.WebmailHTTPS.Path
725 }
726 srv := ensureServe(true, port, "webmail-https at "+path, true)
727 var accountPath string
728 if l.AccountHTTPS.Enabled {
729 accountPath = "/"
730 if l.AccountHTTPS.Path != "" {
731 accountPath = l.AccountHTTPS.Path
732 }
733 }
734 handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
735 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
736 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
737 }
738
739 if l.MetricsHTTP.Enabled {
740 port := config.Port(l.MetricsHTTP.Port, 8010)
741 srv := ensureServe(false, port, "metrics-http", false)
742 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
743 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
744 if r.URL.Path != "/" {
745 http.NotFound(w, r)
746 return
747 } else if r.Method != "GET" {
748 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
749 return
750 }
751 w.Header().Set("Content-Type", "text/html")
752 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
753 })))
754 }
755 if l.AutoconfigHTTPS.Enabled {
756 port := config.Port(l.AutoconfigHTTPS.Port, 443)
757 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https", false)
758 if l.AutoconfigHTTPS.NonTLS {
759 ensureACMEHTTP01(srv)
760 }
761 autoconfigMatch := func(ipdom dns.IPDomain) bool {
762 dom := ipdom.Domain
763 if dom.IsZero() {
764 return false
765 }
766 // Thunderbird requests an autodiscovery URL at the email address domain name, so
767 // autoconfig prefix is optional.
768 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
769 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
770 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
771 }
772 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
773 // use the mail server's host name.
774 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
775 return true
776 }
777 dc, ok := mox.Conf.Domain(dom)
778 return ok && !dc.ReportsOnly
779 }
780 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
781 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
782 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
783 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
784 }
785 if l.MTASTSHTTPS.Enabled {
786 port := config.Port(l.MTASTSHTTPS.Port, 443)
787 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https", false)
788 if l.MTASTSHTTPS.NonTLS {
789 ensureACMEHTTP01(srv)
790 }
791 mtastsMatch := func(ipdom dns.IPDomain) bool {
792 // todo: may want to check this against the configured domains, could in theory be just a webserver.
793 dom := ipdom.Domain
794 if dom.IsZero() {
795 return false
796 }
797 return strings.HasPrefix(dom.ASCII, "mta-sts.")
798 }
799 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
800 }
801 if l.PprofHTTP.Enabled {
802 // Importing net/http/pprof registers handlers on the default serve mux.
803 port := config.Port(l.PprofHTTP.Port, 8011)
804 if _, ok := portServe[port]; ok {
805 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
806 }
807 srv := &serve{[]string{"pprof-http"}, nil, false, nil, false, nil}
808 portServe[port] = srv
809 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
810 }
811 if l.WebserverHTTP.Enabled {
812 port := config.Port(l.WebserverHTTP.Port, 80)
813 srv := ensureServe(false, port, "webserver-http", false)
814 srv.Webserver = true
815 ensureACMEHTTP01(srv)
816 }
817 if l.WebserverHTTPS.Enabled {
818 port := config.Port(l.WebserverHTTPS.Port, 443)
819 srv := ensureServe(true, port, "webserver-https", false)
820 srv.Webserver = true
821 }
822
823 if l.TLS != nil && l.TLS.ACME != "" {
824 hosts := map[dns.Domain]struct{}{
825 mox.Conf.Static.HostnameDomain: {},
826 }
827 if l.HostnameDomain.ASCII != "" {
828 hosts[l.HostnameDomain] = struct{}{}
829 }
830 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
831 // presence of TLS certificates for.
832 for _, name := range mox.Conf.Domains() {
833 if dom, err := dns.ParseDomain(name); err != nil {
834 pkglog.Errorx("parsing domain from config", err)
835 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
836 // Do not gather autoconfig name if we aren't accepting email for this domain.
837 continue
838 }
839
840 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
841 if err != nil {
842 pkglog.Errorx("parsing domain from config for autoconfig", err)
843 } else {
844 hosts[autoconfdom] = struct{}{}
845 }
846 }
847
848 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
849 ensureManagerHosts[m] = hosts
850 }
851
852 for _, srv := range portServe {
853 sortPathHandlers(srv.SystemHandlers)
854 sortPathHandlers(srv.ServiceHandlers)
855 }
856
857 return portServe
858}
859
860func sortPathHandlers(l []pathHandler) {
861 sort.Slice(l, func(i, j int) bool {
862 a := l[i].Path
863 b := l[j].Path
864 if len(a) == len(b) {
865 // For consistent order.
866 return a < b
867 }
868 // Longest paths first.
869 return len(a) > len(b)
870 })
871}
872
873// functions to be launched in goroutine that will serve on a listener.
874var servers []func()
875
876// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
877// immediately after startup. We only do so for our explicit listener hostnames,
878// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
879// do request autoconfig, otherwise clients may run into their timeouts waiting for
880// the certificate to be given during the first https connection.
881var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
882
883// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
884func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler) {
885 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
886
887 var protocol string
888 var ln net.Listener
889 var err error
890 if tlsConfig == nil {
891 protocol = "http"
892 if os.Getuid() == 0 {
893 pkglog.Print("http listener",
894 slog.String("name", name),
895 slog.String("kinds", strings.Join(kinds, ",")),
896 slog.String("address", addr))
897 }
898 ln, err = mox.Listen(mox.Network(ip), addr)
899 if err != nil {
900 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
901 }
902 } else {
903 protocol = "https"
904 if os.Getuid() == 0 {
905 pkglog.Print("https listener",
906 slog.String("name", name),
907 slog.String("kinds", strings.Join(kinds, ",")),
908 slog.String("address", addr))
909 }
910 ln, err = mox.Listen(mox.Network(ip), addr)
911 if err != nil {
912 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
913 }
914 ln = tls.NewListener(ln, tlsConfig)
915 }
916
917 server := &http.Server{
918 Handler: handler,
919 // Clone because our multiple Server.Serve calls modify config concurrently leading to data race.
920 TLSConfig: tlsConfig.Clone(),
921 ReadHeaderTimeout: 30 * time.Second,
922 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
923 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
924 }
925 serve := func() {
926 err := server.Serve(ln)
927 pkglog.Fatalx(protocol+": serve", err)
928 }
929 servers = append(servers, serve)
930}
931
932// Serve starts serving on the initialized listeners.
933func Serve() {
934 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
935
936 go webaccount.ImportManage()
937
938 for _, serve := range servers {
939 go serve()
940 }
941 servers = nil
942
943 go func() {
944 time.Sleep(1 * time.Second)
945 i := 0
946 for m, hosts := range ensureManagerHosts {
947 for host := range hosts {
948 // Check if certificate is already available. If so, we don't print as much after a
949 // restart, and finish more quickly if only a few certificates are missing/old.
950 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
951 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
952 } else if avail {
953 continue
954 }
955
956 if i >= 10 {
957 // Just in case someone adds quite some domains to their config. We don't want to
958 // hit any ACME rate limits.
959 return
960 }
961 if i > 0 {
962 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
963 time.Sleep(10 * time.Second)
964 }
965 i++
966
967 hello := &tls.ClientHelloInfo{
968 ServerName: host.ASCII,
969
970 // Make us fetch an ECDSA P256 cert.
971 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
972 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
973 SupportedCurves: []tls.CurveID{tls.CurveP256},
974 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
975 SupportedVersions: []uint16{tls.VersionTLS13},
976 }
977 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
978 if _, err := m.Manager.GetCertificate(hello); err != nil {
979 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))
980 }
981 }
982 }
983 }()
984}
985