mox/http/web.go
Mechiel Lukkien cb229cb6cf
mox!
2023-01-30 14:27:06 +01:00

240 lines
7.7 KiB
Go

// Package http provides HTTP listeners/servers, for
// autoconfiguration/autodiscovery, the account and admin web interface and
// MTA-STS policies.
package http
import (
"crypto/tls"
"fmt"
golog "log"
"net"
"net/http"
"strings"
"time"
_ "net/http/pprof"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/mjl-/mox/config"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
)
var xlog = mlog.New("http")
// Set some http headers that should prevent potential abuse. Better safe than sorry.
func safeHeaders(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("X-Frame-Options", "deny")
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Content-Security-Policy", "default-src 'self' 'unsafe-inline' data:")
h.Set("Referrer-Policy", "same-origin")
fn(w, r)
}
}
// ListenAndServe starts listeners for HTTP, including those required for ACME to
// generate TLS certificates.
func ListenAndServe() {
type serve struct {
kinds []string
tlsConfig *tls.Config
mux *http.ServeMux
}
for name, l := range mox.Conf.Static.Listeners {
portServe := map[int]serve{}
var ensureServe func(https bool, port int, kind string) serve
ensureServe = func(https bool, port int, kind string) serve {
s, ok := portServe[port]
if !ok {
s = serve{nil, nil, &http.ServeMux{}}
}
s.kinds = append(s.kinds, kind)
if https && port == 443 && l.TLS.ACME != "" {
s.tlsConfig = l.TLS.ACMEConfig
} else if https {
s.tlsConfig = l.TLS.Config
if l.TLS.ACME != "" {
ensureServe(true, 443, "acme-tls-alpn-01")
}
}
portServe[port] = s
return s
}
if l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled {
ensureServe(true, 443, "acme-tls-alpn01")
}
if l.AdminHTTP.Enabled {
srv := ensureServe(false, config.Port(l.AdminHTTP.Port, 80), "admin-http")
srv.mux.HandleFunc("/", safeHeaders(adminIndex))
srv.mux.HandleFunc("/admin/", safeHeaders(adminHandle))
srv.mux.HandleFunc("/account/", safeHeaders(accountHandle))
}
if l.AdminHTTPS.Enabled {
srv := ensureServe(true, config.Port(l.AdminHTTPS.Port, 443), "admin-https")
srv.mux.HandleFunc("/", safeHeaders(adminIndex))
srv.mux.HandleFunc("/admin/", safeHeaders(adminHandle))
srv.mux.HandleFunc("/account/", safeHeaders(accountHandle))
}
if l.MetricsHTTP.Enabled {
srv := ensureServe(false, config.Port(l.MetricsHTTP.Port, 8010), "metrics-http")
srv.mux.Handle("/metrics", safeHeaders(promhttp.Handler().ServeHTTP))
srv.mux.HandleFunc("/", safeHeaders(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
} else if r.Method != "GET" {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<html><body>see <a href="/metrics">/metrics</a></body></html>`)
}))
}
if l.AutoconfigHTTPS.Enabled {
srv := ensureServe(true, 443, "autoconfig-https")
srv.mux.HandleFunc("/mail/config-v1.1.xml", safeHeaders(autoconfHandle(l)))
srv.mux.HandleFunc("/autodiscover/autodiscover.xml", safeHeaders(autodiscoverHandle(l)))
}
if l.MTASTSHTTPS.Enabled {
srv := ensureServe(true, 443, "mtasts-https")
srv.mux.HandleFunc("/.well-known/mta-sts.txt", safeHeaders(mtastsPolicyHandle))
}
if l.PprofHTTP.Enabled {
// Importing net/http/pprof registers handlers on the default serve mux.
port := config.Port(l.PprofHTTP.Port, 8011)
if _, ok := portServe[port]; ok {
xlog.Fatal("cannot serve pprof on same endpoint as other http services")
}
portServe[port] = serve{[]string{"pprof-http"}, nil, http.DefaultServeMux}
}
// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
// immediately after startup. We only do so for our explicitly hostnames, not for
// autoconfig or mta-sts DNS records, they can be requested on demand (perhaps
// never).
ensureHosts := map[dns.Domain]struct{}{}
if l.TLS != nil && l.TLS.ACME != "" {
m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
m.AllowHostname(mox.Conf.Static.HostnameDomain)
ensureHosts[mox.Conf.Static.HostnameDomain] = struct{}{}
if l.HostnameDomain.ASCII != "" {
m.AllowHostname(l.HostnameDomain)
ensureHosts[l.HostnameDomain] = struct{}{}
}
go func() {
// Just in case someone adds quite some domains to their config. We don't want to
// hit any ACME rate limits.
if len(ensureHosts) > 10 {
return
}
time.Sleep(1 * time.Second)
i := 0
for hostname := range ensureHosts {
if i > 0 {
// Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
time.Sleep(10 * time.Second)
}
i++
hello := &tls.ClientHelloInfo{
ServerName: hostname.ASCII,
// Make us fetch an ECDSA P256 cert.
// We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
SupportedCurves: []tls.CurveID{tls.CurveP256},
SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
SupportedVersions: []uint16{tls.VersionTLS13},
}
xlog.Print("ensuring certificate availability", mlog.Field("hostname", hostname))
if _, err := m.Manager.GetCertificate(hello); err != nil {
xlog.Errorx("requesting automatic certificate", err, mlog.Field("hostname", hostname))
}
}
}()
}
for port, srv := range portServe {
for _, ip := range l.IPs {
listenAndServe(ip, port, srv.tlsConfig, name, srv.kinds, srv.mux)
}
}
}
}
func adminIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
if r.Method != "GET" {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
const html = `<!doctype html>
<html>
<head>
<title>mox</title>
<meta name="viewport" content="width=device-width" />
<style>
body, html { font-family: ubuntu, lato, sans-serif; font-size: 16px; padding: 1em; }
* { margin: 0; padding: 0; box-sizing: border-box; }
h1, h2, h3, h4 { margin-bottom: 1ex; }
h1 { font-size: 1.2rem; }
</style>
</head>
<body>
<h1>mox</h1>
<div><a href="/account/">/account/</a>, for regular login</div>
<div><a href="/admin/">/admin/</a>, for adminstrators</div>
</body>
</html>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
func listenAndServe(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, mux *http.ServeMux) {
addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
var protocol string
var ln net.Listener
var err error
if tlsConfig == nil {
protocol = "http"
xlog.Print("http listener", mlog.Field("name", name), mlog.Field("kinds", strings.Join(kinds, ",")), mlog.Field("address", addr))
ln, err = net.Listen(mox.Network(ip), addr)
if err != nil {
xlog.Fatalx("http: listen"+mox.LinuxSetcapHint(err), err, mlog.Field("addr", addr))
}
} else {
protocol = "https"
xlog.Print("https listener", mlog.Field("name", name), mlog.Field("kinds", strings.Join(kinds, ",")), mlog.Field("address", addr))
ln, err = tls.Listen(mox.Network(ip), addr, tlsConfig)
if err != nil {
xlog.Fatalx("https: listen"+mox.LinuxSetcapHint(err), err, mlog.Field("addr", addr))
}
}
server := &http.Server{
Handler: mux,
TLSConfig: tlsConfig,
ErrorLog: golog.New(mlog.ErrWriter(xlog.Fields(mlog.Field("pkg", "net/http")), mlog.LevelInfo, protocol+" error"), "", 0),
}
go func() {
err := server.Serve(ln)
xlog.Fatalx(protocol+": serve", err)
}()
}