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