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

@ -63,6 +63,16 @@ var (
var ErrConfig = errors.New("config error")
// Set by packages webadmin, webaccount, webmail, webapisrv to prevent cyclic dependencies.
var NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
var NewWebaccountHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
var NewWebmailHandler = func(maxMsgSize int64, basePath string, isForwarded bool, accountPath string) http.Handler {
return nopHandler
}
var NewWebapiHandler = func(maxMsgSize int64, basePath string, isForwarded bool) http.Handler { return nopHandler }
var nopHandler = http.HandlerFunc(nil)
// Config as used in the code, a processed version of what is in the config file.
//
// Use methods to lookup a domain/account/address in the dynamic configuration.
@ -262,6 +272,13 @@ func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, d
return
}
func (c *Config) IsClientSettingsDomain(d dns.Domain) (is bool) {
c.withDynamicLock(func() {
_, is = c.Dynamic.ClientSettingDomains[d]
})
return
}
func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
for _, l := range c.Static.Listeners {
if l.TLS == nil || l.TLS.ACME == "" {
@ -1124,6 +1141,7 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
checkRoutes("global routes", c.Routes)
// Validate domains.
c.ClientSettingDomains = map[dns.Domain]struct{}{}
for d, domain := range c.Domains {
dnsdomain, err := dns.ParseDomain(d)
if err != nil {
@ -1140,6 +1158,7 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
addErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
}
domain.ClientSettingsDNSDomain = csd
c.ClientSettingDomains[csd] = struct{}{}
}
for _, sign := range domain.DKIM.Sign {
@ -1814,6 +1833,29 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
}
}
}
if wh.WebInternal != nil {
n++
wi := wh.WebInternal
if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
addErrorf("webinternal %s %s: base path %q must start and end with /", wh.Domain, wh.PathRegexp, wi.BasePath)
}
// todo: we could make maxMsgSize and accountPath configurable
const isForwarded = false
switch wi.Service {
case "admin":
wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
case "account":
wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
case "webmail":
accountPath := ""
wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
case "webapi":
wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
default:
addErrorf("webinternal %s %s: unknown service %q", wh.Domain, wh.PathRegexp, wi.Service)
}
wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
}
if n != 1 {
addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
}