improve http request handling for internal services and multiple domains

per listener, you could enable the admin/account/webmail/webapi handlers. but
that would serve those services on their configured paths (/admin/, /,
/webmail/, /webapi/) on all domains mox would be webserving, including any
non-mail domains. so your www.example/admin/ would be serving the admin web
interface, with no way to disabled that.

with this change, the admin interface is only served on requests to (based on
Host header):
- ip addresses
- the listener host name (explicitly configured in the listener, with fallback
  to global hostname)
- "localhost" (for ssh tunnel/forwarding scenario's)

the account/webmail/webapi interfaces are served on the same domains as the
admin interface, and additionally:
- the client settings domains, as optionally configured in each Domain in
  domains.conf. typically "mail.<yourdomain>".

this means the internal services are no longer served on other domains
configured in the webserver, e.g. www.example.org/admin/ will not be handled
specially.

the order of evaluation of routes/services is also changed:
before this change, the internal handlers would always be evaluated first.
with this change, only the system handlers for
MTA-STS/autoconfig/ACME-validation will be evaluated first. then the webserver
handlers. and finally the internal services (admin/account/webmail/webapi).
this allows an admin to configure overrides for some of the domains (per
hostname-matching rules explained above) that would normally serve these
services.

webserver handlers can now be configured that pass the request to an internal
service: in addition to the existing static/redirect/forward config options,
there is now an "internal" config option, naming the service
(admin/account/webmail/webapi) for handling the request. this allows enabling
the internal services on custom domains.

for issue #160 by TragicLifeHu, thanks for reporting!
This commit is contained in:
Mechiel Lukkien
2024-05-11 11:13:14 +02:00
parent 9152384fd3
commit 614576e409
20 changed files with 746 additions and 350 deletions

View File

@ -351,37 +351,40 @@ func (w *loggingWriter) Done() {
pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
}
// Set some http headers that should prevent potential abuse. Better safe than sorry.
func safeHeaders(fn http.Handler) http.Handler {
return http.HandlerFunc(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.ServeHTTP(w, r)
})
}
// Built-in handlers, e.g. mta-sts and autoconfig.
type pathHandler struct {
Name string // For logging/metrics.
HostMatch func(dom dns.Domain) bool // If not nil, called to see if domain of requests matches. Only called if requested host is a valid domain.
Path string // Path to register, like on http.ServeMux.
Name string // For logging/metrics.
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.
Path string // Path to register, like on http.ServeMux.
Handler http.Handler
}
type serve struct {
Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https).
TLSConfig *tls.Config
PathHandlers []pathHandler // Sorted, longest first.
Webserver bool // Whether serving WebHandler. PathHandlers are always evaluated before WebHandlers.
Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https).
TLSConfig *tls.Config
// SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
// overridden by WebHandlers. WebHandlers are evaluated next, and the internal
// service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
// interfaces) last. WebHandlers can also pass requests to the internal servers.
// This order allows admins to serve other content on domains serving the mox.conf
// internal services.
SystemHandlers []pathHandler // Sorted, longest first.
Webserver bool
ServiceHandlers []pathHandler // Sorted, longest first.
}
// Handle registers a named handler for a path and optional host. If path ends with
// a slash, it is used as prefix match, otherwise a full path match is required. If
// hostOpt is set, only requests to those host are handled by this handler.
func (s *serve) Handle(name string, hostMatch func(dns.Domain) bool, path string, fn http.Handler) {
s.PathHandlers = append(s.PathHandlers, pathHandler{name, hostMatch, path, fn})
// SystemHandle registers a named system handler for a path and optional host. If
// path ends with a slash, it is used as prefix match, otherwise a full path match
// is required. If hostOpt is set, only requests to those host are handled by this
// handler.
func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
}
// Like SystemHandle, but for internal services "admin", "account", "webmail",
// "webapi" configured in the mox.conf Listener.
func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
}
var (
@ -452,28 +455,44 @@ func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
r.URL.Path += "/"
}
var dom dns.Domain
host := r.Host
nhost, _, err := net.SplitHostPort(host)
if err == nil {
host = nhost
}
// host could be an IP, some handles may match, not an error.
dom, domErr := dns.ParseDomain(host)
ipdom := dns.IPDomain{IP: net.ParseIP(host)}
if ipdom.IP == nil {
dom, domErr := dns.ParseDomain(host)
if domErr == nil {
ipdom = dns.IPDomain{Domain: dom}
}
}
for _, h := range s.PathHandlers {
if h.HostMatch != nil && (domErr != nil || !h.HostMatch(dom)) {
continue
handle := func(h pathHandler) bool {
if h.HostMatch != nil && !h.HostMatch(ipdom) {
return false
}
if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
nw.Handler = h.Name
nw.Compress = true
h.Handler.ServeHTTP(nw, r)
return true
}
return false
}
for _, h := range s.SystemHandlers {
if handle(h) {
return
}
}
if s.Webserver && domErr == nil {
if WebHandle(nw, r, dom) {
if s.Webserver {
if WebHandle(nw, r, ipdom) {
return
}
}
for _, h := range s.ServiceHandlers {
if handle(h) {
return
}
}
@ -481,284 +500,31 @@ func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
http.NotFound(nw, r)
}
func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
// Helpfully redirect user to version with ending slash.
if path != "/" && strings.HasSuffix(path, "/") {
handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, path, http.StatusSeeOther)
}))
srv.ServiceHandle(name, hostMatch, path[:len(path)-1], handler)
}
}
// Listen binds to sockets for HTTP listeners, including those required for ACME to
// generate TLS certificates. It stores the listeners so Serve can start serving them.
func Listen() {
redirectToTrailingSlash := func(srv *serve, name, path string) {
// Helpfully redirect user to version with ending slash.
if path != "/" && strings.HasSuffix(path, "/") {
handler := safeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, path, http.StatusSeeOther)
}))
srv.Handle(name, nil, path[:len(path)-1], handler)
}
}
// Initialize listeners in deterministic order for the same potential error
// messages.
names := maps.Keys(mox.Conf.Static.Listeners)
sort.Strings(names)
for _, name := range names {
l := mox.Conf.Static.Listeners[name]
portServe := map[int]*serve{}
var ensureServe func(https bool, port int, kind string) *serve
ensureServe = func(https bool, port int, kind string) *serve {
s := portServe[port]
if s == nil {
s = &serve{nil, nil, nil, false}
portServe[port] = s
}
s.Kinds = append(s.Kinds, kind)
if https && l.TLS.ACME != "" {
s.TLSConfig = l.TLS.ACMEConfig
} else if https {
s.TLSConfig = l.TLS.Config
if l.TLS.ACME != "" {
tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
ensureServe(true, tlsport, "acme-tls-alpn-01")
}
}
return s
}
if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
ensureServe(true, port, "acme-tls-alpn-01")
}
if l.AccountHTTP.Enabled {
port := config.Port(l.AccountHTTP.Port, 80)
path := "/"
if l.AccountHTTP.Path != "" {
path = l.AccountHTTP.Path
}
srv := ensureServe(false, port, "account-http at "+path)
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
srv.Handle("account", nil, path, handler)
redirectToTrailingSlash(srv, "account", path)
}
if l.AccountHTTPS.Enabled {
port := config.Port(l.AccountHTTPS.Port, 443)
path := "/"
if l.AccountHTTPS.Path != "" {
path = l.AccountHTTPS.Path
}
srv := ensureServe(true, port, "account-https at "+path)
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
srv.Handle("account", nil, path, handler)
redirectToTrailingSlash(srv, "account", path)
}
if l.AdminHTTP.Enabled {
port := config.Port(l.AdminHTTP.Port, 80)
path := "/admin/"
if l.AdminHTTP.Path != "" {
path = l.AdminHTTP.Path
}
srv := ensureServe(false, port, "admin-http at "+path)
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
srv.Handle("admin", nil, path, handler)
redirectToTrailingSlash(srv, "admin", path)
}
if l.AdminHTTPS.Enabled {
port := config.Port(l.AdminHTTPS.Port, 443)
path := "/admin/"
if l.AdminHTTPS.Path != "" {
path = l.AdminHTTPS.Path
}
srv := ensureServe(true, port, "admin-https at "+path)
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
srv.Handle("admin", nil, path, handler)
redirectToTrailingSlash(srv, "admin", path)
}
maxMsgSize := l.SMTPMaxMessageSize
if maxMsgSize == 0 {
maxMsgSize = config.DefaultMaxMsgSize
}
if l.WebAPIHTTP.Enabled {
port := config.Port(l.WebAPIHTTP.Port, 80)
path := "/webapi/"
if l.WebAPIHTTP.Path != "" {
path = l.WebAPIHTTP.Path
}
srv := ensureServe(false, port, "webapi-http at "+path)
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
srv.Handle("webapi", nil, path, handler)
redirectToTrailingSlash(srv, "webapi", path)
}
if l.WebAPIHTTPS.Enabled {
port := config.Port(l.WebAPIHTTPS.Port, 443)
path := "/webapi/"
if l.WebAPIHTTPS.Path != "" {
path = l.WebAPIHTTPS.Path
}
srv := ensureServe(true, port, "webapi-https at "+path)
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
srv.Handle("webapi", nil, path, handler)
redirectToTrailingSlash(srv, "webapi", path)
}
if l.WebmailHTTP.Enabled {
port := config.Port(l.WebmailHTTP.Port, 80)
path := "/webmail/"
if l.WebmailHTTP.Path != "" {
path = l.WebmailHTTP.Path
}
srv := ensureServe(false, port, "webmail-http at "+path)
var accountPath string
if l.AccountHTTP.Enabled {
accountPath = "/"
if l.AccountHTTP.Path != "" {
accountPath = l.AccountHTTP.Path
}
}
handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
srv.Handle("webmail", nil, path, handler)
redirectToTrailingSlash(srv, "webmail", path)
}
if l.WebmailHTTPS.Enabled {
port := config.Port(l.WebmailHTTPS.Port, 443)
path := "/webmail/"
if l.WebmailHTTPS.Path != "" {
path = l.WebmailHTTPS.Path
}
srv := ensureServe(true, port, "webmail-https at "+path)
var accountPath string
if l.AccountHTTPS.Enabled {
accountPath = "/"
if l.AccountHTTPS.Path != "" {
accountPath = l.AccountHTTPS.Path
}
}
handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
srv.Handle("webmail", nil, path, handler)
redirectToTrailingSlash(srv, "webmail", path)
}
if l.MetricsHTTP.Enabled {
port := config.Port(l.MetricsHTTP.Port, 8010)
srv := ensureServe(false, port, "metrics-http")
srv.Handle("metrics", nil, "/metrics", safeHeaders(promhttp.Handler()))
srv.Handle("metrics", nil, "/", safeHeaders(http.HandlerFunc(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 {
port := config.Port(l.AutoconfigHTTPS.Port, 443)
srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https")
autoconfigMatch := func(dom dns.Domain) bool {
// Thunderbird requests an autodiscovery URL at the email address domain name, so
// autoconfig prefix is optional.
if strings.HasPrefix(dom.ASCII, "autoconfig.") {
dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
}
// Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
// use the mail server's host name.
if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
return true
}
dc, ok := mox.Conf.Domain(dom)
return ok && !dc.ReportsOnly
}
srv.Handle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", safeHeaders(http.HandlerFunc(autoconfHandle)))
srv.Handle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", safeHeaders(http.HandlerFunc(autodiscoverHandle)))
srv.Handle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", safeHeaders(http.HandlerFunc(mobileconfigHandle)))
srv.Handle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", safeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
}
if l.MTASTSHTTPS.Enabled {
port := config.Port(l.MTASTSHTTPS.Port, 443)
srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https")
mtastsMatch := func(dom dns.Domain) bool {
// todo: may want to check this against the configured domains, could in theory be just a webserver.
return strings.HasPrefix(dom.ASCII, "mta-sts.")
}
srv.Handle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", safeHeaders(http.HandlerFunc(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 {
pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
}
srv := &serve{[]string{"pprof-http"}, nil, nil, false}
portServe[port] = srv
srv.Handle("pprof", nil, "/", http.DefaultServeMux)
}
if l.WebserverHTTP.Enabled {
port := config.Port(l.WebserverHTTP.Port, 80)
srv := ensureServe(false, port, "webserver-http")
srv.Webserver = true
}
if l.WebserverHTTPS.Enabled {
port := config.Port(l.WebserverHTTPS.Port, 443)
srv := ensureServe(true, port, "webserver-https")
srv.Webserver = true
}
if l.TLS != nil && l.TLS.ACME != "" {
m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
// If we are listening on port 80 for plain http, also register acme http-01
// validation handler.
if srv, ok := portServe[80]; ok && srv.TLSConfig == nil {
srv.Kinds = append(srv.Kinds, "acme-http-01")
srv.Handle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
}
hosts := map[dns.Domain]struct{}{
mox.Conf.Static.HostnameDomain: {},
}
if l.HostnameDomain.ASCII != "" {
hosts[l.HostnameDomain] = struct{}{}
}
// All domains are served on all listeners. Gather autoconfig hostnames to ensure
// presence of TLS certificates for.
for _, name := range mox.Conf.Domains() {
if dom, err := dns.ParseDomain(name); err != nil {
pkglog.Errorx("parsing domain from config", err)
} else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
// Do not gather autoconfig name if we aren't accepting email for this domain.
continue
}
autoconfdom, err := dns.ParseDomain("autoconfig." + name)
if err != nil {
pkglog.Errorx("parsing domain from config for autoconfig", err)
} else {
hosts[autoconfdom] = struct{}{}
}
}
ensureManagerHosts[m] = hosts
}
portServe := portServes(l)
ports := maps.Keys(portServe)
sort.Ints(ports)
for _, port := range ports {
srv := portServe[port]
sort.Slice(srv.PathHandlers, func(i, j int) bool {
a := srv.PathHandlers[i].Path
b := srv.PathHandlers[j].Path
if len(a) == len(b) {
// For consistent order.
return a < b
}
// Longest paths first.
return len(a) > len(b)
})
for _, ip := range l.IPs {
listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv)
}
@ -766,6 +532,300 @@ func Listen() {
}
}
func portServes(l config.Listener) map[int]*serve {
portServe := map[int]*serve{}
// For system/services, we serve on host localhost too, for ssh tunnel scenario's.
localhost := dns.Domain{ASCII: "localhost"}
ldom := l.HostnameDomain
if l.Hostname == "" {
ldom = mox.Conf.Static.HostnameDomain
}
listenerHostMatch := func(host dns.IPDomain) bool {
if host.IsIP() {
return true
}
return host.Domain == ldom || host.Domain == localhost
}
accountHostMatch := func(host dns.IPDomain) bool {
if listenerHostMatch(host) {
return true
}
return mox.Conf.IsClientSettingsDomain(host.Domain)
}
var ensureServe func(https bool, port int, kind string) *serve
ensureServe = func(https bool, port int, kind string) *serve {
s := portServe[port]
if s == nil {
s = &serve{nil, nil, nil, false, nil}
portServe[port] = s
}
s.Kinds = append(s.Kinds, kind)
if https && l.TLS.ACME != "" {
s.TLSConfig = l.TLS.ACMEConfig
} else if https {
s.TLSConfig = l.TLS.Config
if l.TLS.ACME != "" {
tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
ensureServe(true, tlsport, "acme-tls-alpn-01")
}
}
return s
}
if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
ensureServe(true, port, "acme-tls-alpn-01")
}
if l.AccountHTTP.Enabled {
port := config.Port(l.AccountHTTP.Port, 80)
path := "/"
if l.AccountHTTP.Path != "" {
path = l.AccountHTTP.Path
}
srv := ensureServe(false, port, "account-http at "+path)
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
srv.ServiceHandle("account", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "account", path)
}
if l.AccountHTTPS.Enabled {
port := config.Port(l.AccountHTTPS.Port, 443)
path := "/"
if l.AccountHTTPS.Path != "" {
path = l.AccountHTTPS.Path
}
srv := ensureServe(true, port, "account-https at "+path)
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
srv.ServiceHandle("account", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "account", path)
}
if l.AdminHTTP.Enabled {
port := config.Port(l.AdminHTTP.Port, 80)
path := "/admin/"
if l.AdminHTTP.Path != "" {
path = l.AdminHTTP.Path
}
srv := ensureServe(false, port, "admin-http at "+path)
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
srv.ServiceHandle("admin", listenerHostMatch, path, handler)
redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
}
if l.AdminHTTPS.Enabled {
port := config.Port(l.AdminHTTPS.Port, 443)
path := "/admin/"
if l.AdminHTTPS.Path != "" {
path = l.AdminHTTPS.Path
}
srv := ensureServe(true, port, "admin-https at "+path)
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
srv.ServiceHandle("admin", listenerHostMatch, path, handler)
redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
}
maxMsgSize := l.SMTPMaxMessageSize
if maxMsgSize == 0 {
maxMsgSize = config.DefaultMaxMsgSize
}
if l.WebAPIHTTP.Enabled {
port := config.Port(l.WebAPIHTTP.Port, 80)
path := "/webapi/"
if l.WebAPIHTTP.Path != "" {
path = l.WebAPIHTTP.Path
}
srv := ensureServe(false, port, "webapi-http at "+path)
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
srv.ServiceHandle("webapi", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
}
if l.WebAPIHTTPS.Enabled {
port := config.Port(l.WebAPIHTTPS.Port, 443)
path := "/webapi/"
if l.WebAPIHTTPS.Path != "" {
path = l.WebAPIHTTPS.Path
}
srv := ensureServe(true, port, "webapi-https at "+path)
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
srv.ServiceHandle("webapi", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
}
if l.WebmailHTTP.Enabled {
port := config.Port(l.WebmailHTTP.Port, 80)
path := "/webmail/"
if l.WebmailHTTP.Path != "" {
path = l.WebmailHTTP.Path
}
srv := ensureServe(false, port, "webmail-http at "+path)
var accountPath string
if l.AccountHTTP.Enabled {
accountPath = "/"
if l.AccountHTTP.Path != "" {
accountPath = l.AccountHTTP.Path
}
}
handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
srv.ServiceHandle("webmail", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
}
if l.WebmailHTTPS.Enabled {
port := config.Port(l.WebmailHTTPS.Port, 443)
path := "/webmail/"
if l.WebmailHTTPS.Path != "" {
path = l.WebmailHTTPS.Path
}
srv := ensureServe(true, port, "webmail-https at "+path)
var accountPath string
if l.AccountHTTPS.Enabled {
accountPath = "/"
if l.AccountHTTPS.Path != "" {
accountPath = l.AccountHTTPS.Path
}
}
handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
srv.ServiceHandle("webmail", accountHostMatch, path, handler)
redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
}
if l.MetricsHTTP.Enabled {
port := config.Port(l.MetricsHTTP.Port, 8010)
srv := ensureServe(false, port, "metrics-http")
srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(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 {
port := config.Port(l.AutoconfigHTTPS.Port, 443)
srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https")
autoconfigMatch := func(ipdom dns.IPDomain) bool {
dom := ipdom.Domain
if dom.IsZero() {
return false
}
// Thunderbird requests an autodiscovery URL at the email address domain name, so
// autoconfig prefix is optional.
if strings.HasPrefix(dom.ASCII, "autoconfig.") {
dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
}
// Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
// use the mail server's host name.
if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
return true
}
dc, ok := mox.Conf.Domain(dom)
return ok && !dc.ReportsOnly
}
srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
}
if l.MTASTSHTTPS.Enabled {
port := config.Port(l.MTASTSHTTPS.Port, 443)
srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https")
mtastsMatch := func(ipdom dns.IPDomain) bool {
// todo: may want to check this against the configured domains, could in theory be just a webserver.
dom := ipdom.Domain
if dom.IsZero() {
return false
}
return strings.HasPrefix(dom.ASCII, "mta-sts.")
}
srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(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 {
pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
}
srv := &serve{[]string{"pprof-http"}, nil, nil, false, nil}
portServe[port] = srv
srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
}
if l.WebserverHTTP.Enabled {
port := config.Port(l.WebserverHTTP.Port, 80)
srv := ensureServe(false, port, "webserver-http")
srv.Webserver = true
}
if l.WebserverHTTPS.Enabled {
port := config.Port(l.WebserverHTTPS.Port, 443)
srv := ensureServe(true, port, "webserver-https")
srv.Webserver = true
}
if l.TLS != nil && l.TLS.ACME != "" {
m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
// If we are listening on port 80 for plain http, also register acme http-01
// validation handler.
if srv, ok := portServe[80]; ok && srv.TLSConfig == nil {
srv.Kinds = append(srv.Kinds, "acme-http-01")
srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
}
hosts := map[dns.Domain]struct{}{
mox.Conf.Static.HostnameDomain: {},
}
if l.HostnameDomain.ASCII != "" {
hosts[l.HostnameDomain] = struct{}{}
}
// All domains are served on all listeners. Gather autoconfig hostnames to ensure
// presence of TLS certificates for.
for _, name := range mox.Conf.Domains() {
if dom, err := dns.ParseDomain(name); err != nil {
pkglog.Errorx("parsing domain from config", err)
} else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
// Do not gather autoconfig name if we aren't accepting email for this domain.
continue
}
autoconfdom, err := dns.ParseDomain("autoconfig." + name)
if err != nil {
pkglog.Errorx("parsing domain from config for autoconfig", err)
} else {
hosts[autoconfdom] = struct{}{}
}
}
ensureManagerHosts[m] = hosts
}
for _, srv := range portServe {
sortPathHandlers(srv.SystemHandlers)
sortPathHandlers(srv.ServiceHandlers)
}
return portServe
}
func sortPathHandlers(l []pathHandler) {
sort.Slice(l, func(i, j int) bool {
a := l[i].Path
b := l[j].Path
if len(a) == len(b) {
// For consistent order.
return a < b
}
// Longest paths first.
return len(a) > len(b)
})
}
// functions to be launched in goroutine that will serve on a listener.
var servers []func()

View File

@ -6,10 +6,8 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/mox-"
)
@ -19,20 +17,8 @@ func TestServeHTTP(t *testing.T) {
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
mox.MustLoadConfig(true, false)
srv := &serve{
PathHandlers: []pathHandler{
{
HostMatch: func(dom dns.Domain) bool {
return strings.HasPrefix(dom.ASCII, "mta-sts.")
},
Path: "/.well-known/mta-sts.txt",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("mta-sts!"))
}),
},
},
Webserver: true,
}
portSrvs := portServes(mox.Conf.Static.Listeners["local"])
srv := portSrvs[80]
test := func(method, target string, expCode int, expContent string, expHeaders map[string]string) {
t.Helper()
@ -43,22 +29,22 @@ func TestServeHTTP(t *testing.T) {
srv.ServeHTTP(rw, req)
resp := rw.Result()
if resp.StatusCode != expCode {
t.Fatalf("got statuscode %d, expected %d", resp.StatusCode, expCode)
t.Errorf("got statuscode %d, expected %d", resp.StatusCode, expCode)
}
if expContent != "" {
s := rw.Body.String()
if s != expContent {
t.Fatalf("got response data %q, expected %q", s, expContent)
t.Errorf("got response data %q, expected %q", s, expContent)
}
}
for k, v := range expHeaders {
if xv := resp.Header.Get(k); xv != v {
t.Fatalf("got %q for header %q, expected %q", xv, k, v)
t.Errorf("got %q for header %q, expected %q", xv, k, v)
}
}
}
test("GET", "http://mta-sts.mox.example/.well-known/mta-sts.txt", http.StatusOK, "mta-sts!", nil)
test("GET", "http://mta-sts.mox.example/.well-known/mta-sts.txt", http.StatusOK, "version: STSv1\nmode: enforce\nmax_age: 86400\nmx: mox.example\n", nil)
test("GET", "http://mox.example/.well-known/mta-sts.txt", http.StatusNotFound, "", nil) // mta-sts endpoint not in this domain.
test("GET", "http://mta-sts.mox.example/static/", http.StatusNotFound, "", nil) // static not served on this domain.
test("GET", "http://mta-sts.mox.example/other", http.StatusNotFound, "", nil)
@ -66,4 +52,24 @@ func TestServeHTTP(t *testing.T) {
test("GET", "http://mox.example/static/index.html", http.StatusOK, "html\n", map[string]string{"X-Test": "mox"})
test("GET", "http://mox.example/static/dir/", http.StatusOK, "", map[string]string{"X-Test": "mox"}) // Dir listing.
test("GET", "http://mox.example/other", http.StatusNotFound, "", nil)
// Webmail on IP, localhost, mail host, clientsettingsdomain, not others.
test("GET", "http://127.0.0.1/webmail/", http.StatusOK, "", nil)
test("GET", "http://localhost/webmail/", http.StatusOK, "", nil)
test("GET", "http://mox.example/webmail/", http.StatusOK, "", nil)
test("GET", "http://mail.mox.example/webmail/", http.StatusOK, "", nil)
test("GET", "http://mail.other.example/webmail/", http.StatusNotFound, "", nil)
test("GET", "http://remotehost/webmail/", http.StatusNotFound, "", nil)
// admin on IP, localhost, mail host, not clientsettingsdomain.
test("GET", "http://127.0.0.1/admin/", http.StatusOK, "", nil)
test("GET", "http://localhost/admin/", http.StatusOK, "", nil)
test("GET", "http://mox.example/admin/", http.StatusPermanentRedirect, "", nil) // Override by WebHandler.
test("GET", "http://mail.mox.example/admin/", http.StatusNotFound, "", nil)
// account is off.
test("GET", "http://127.0.0.1/", http.StatusNotFound, "", nil)
test("GET", "http://localhost/", http.StatusNotFound, "", nil)
test("GET", "http://mox.example/", http.StatusNotFound, "", nil)
test("GET", "http://mail.mox.example/", http.StatusNotFound, "", nil)
}

View File

@ -46,13 +46,13 @@ func recvid(r *http.Request) string {
// WebHandle runs after the built-in handlers for mta-sts, autoconfig, etc.
// If no handler matched, false is returned.
// WebHandle sets w.Name to that of the matching handler.
func WebHandle(w *loggingWriter, r *http.Request, host dns.Domain) (handled bool) {
func WebHandle(w *loggingWriter, r *http.Request, host dns.IPDomain) (handled bool) {
conf := mox.Conf.DynamicConfig()
redirects := conf.WebDNSDomainRedirects
handlers := conf.WebHandlers
for from, to := range redirects {
if host != from {
if host.Domain != from {
continue
}
u := r.URL
@ -64,7 +64,7 @@ func WebHandle(w *loggingWriter, r *http.Request, host dns.Domain) (handled bool
}
for _, h := range handlers {
if host != h.DNSDomain {
if host.Domain != h.DNSDomain {
continue
}
loc := h.Path.FindStringIndex(r.URL.Path)
@ -99,6 +99,10 @@ func WebHandle(w *loggingWriter, r *http.Request, host dns.Domain) (handled bool
w.Handler = h.Name
return true
}
if h.WebInternal != nil && HandleInternal(h.WebInternal, w, r) {
w.Handler = h.Name
return true
}
}
w.Compress = false
return false
@ -396,6 +400,12 @@ func HandleRedirect(h *config.WebRedirect, w http.ResponseWriter, r *http.Reques
return true
}
// HandleInternal passes the request to an internal service.
func HandleInternal(h *config.WebInternal, w http.ResponseWriter, r *http.Request) (handled bool) {
h.Handler.ServeHTTP(w, r)
return true
}
// HandleForward handles a request by forwarding it to another webserver and
// passing the response on. I.e. a reverse proxy. It handles websocket
// connections by monitoring the websocket handshake and then just passing along the

View File

@ -134,6 +134,10 @@ func TestWebserver(t *testing.T) {
test("GET", "http://mox.example/bogus", nil, http.StatusNotFound, "", nil) // path not registered.
test("GET", "http://bogus.mox.example/static/", nil, http.StatusNotFound, "", nil) // domain not registered.
test("GET", "http://mox.example/xadmin/", nil, http.StatusOK, "", nil) // internal admin service
test("GET", "http://mox.example/xaccount/", nil, http.StatusOK, "", nil) // internal account service
test("GET", "http://mox.example/xwebmail/", nil, http.StatusOK, "", nil) // internal webmail service
test("GET", "http://mox.example/xwebapi/v0/", nil, http.StatusOK, "", nil) // internal webapi service
npaths := len(staticgzcache.paths)
if npaths != 1 {
@ -335,5 +339,4 @@ func TestWebsocket(t *testing.T) {
w.WriteHeader(http.StatusSwitchingProtocols)
})
test("GET", wsreqhdrs, http.StatusSwitchingProtocols, wsresphdrs)
}