Skip to content

Commit

Permalink
improve http request handling for internal services and multiple domains
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
mjl- committed May 11, 2024
1 parent 9152384 commit 614576e
Show file tree
Hide file tree
Showing 20 changed files with 722 additions and 326 deletions.
32 changes: 27 additions & 5 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/tls"
"crypto/x509"
"net"
"net/http"
"net/url"
"reflect"
"regexp"
Expand Down Expand Up @@ -109,12 +110,13 @@ type Dynamic struct {
Domains map[string]Domain `sconf-doc:"NOTE: This config file is in 'sconf' format. Indent with tabs. Comments must be on their own line, they don't end a line. Do not escape or quote strings. Details: https://pkg.go.dev/github.com/mjl-/sconf.\n\n\nDomains for which email is accepted. For internationalized domains, use their IDNA names in UTF-8."`
Accounts map[string]Account `sconf-doc:"Accounts represent mox users, each with a password and email address(es) to which email can be delivered (possibly at different domains). Each account has its own on-disk directory holding its messages and index database. An account name is not an email address."`
WebDomainRedirects map[string]string `sconf:"optional" sconf-doc:"Redirect all requests from domain (key) to domain (value). Always redirects to HTTPS. For plain HTTP redirects, use a WebHandler with a WebRedirect."`
WebHandlers []WebHandler `sconf:"optional" sconf-doc:"Handle webserver requests by serving static files, redirecting or reverse-proxying HTTP(s). The first matching WebHandler will handle the request. Built-in handlers, e.g. for account, admin, autoconfig and mta-sts always run first. If no handler matches, the response status code is file not found (404). If functionality you need is missng, simply forward the requests to an application that can provide the needed functionality."`
WebHandlers []WebHandler `sconf:"optional" sconf-doc:"Handle webserver requests by serving static files, redirecting, reverse-proxying HTTP(s) or passing the request to an internal service. The first matching WebHandler will handle the request. Built-in system handlers, e.g. for ACME validation, autoconfig and mta-sts always run first. Built-in handlers for admin, account, webmail and webapi are evaluated after all handlers, including webhandlers (allowing for overrides of internal services for some domains). If no handler matches, the response status code is file not found (404). If webserver features are missing, forward the requests to an application that provides the needed functionality itself."`
Routes []Route `sconf:"optional" sconf-doc:"Routes for delivering outgoing messages through the queue. Each delivery attempt evaluates account routes, domain routes and finally these global routes. The transport of the first matching route is used in the delivery attempt. If no routes match, which is the default with no configured routes, messages are delivered directly from the queue."`
MonitorDNSBLs []string `sconf:"optional" sconf-doc:"DNS blocklists to periodically check with if IPs we send from are present, without using them for checking incoming deliveries.. Also see DNSBLs in SMTP listeners in mox.conf, which specifies DNSBLs to use both for incoming deliveries and for checking our IPs against. Example DNSBLs: sbl.spamhaus.org, bl.spamcop.net."`

WebDNSDomainRedirects map[dns.Domain]dns.Domain `sconf:"-" json:"-"`
MonitorDNSBLZones []dns.Domain `sconf:"-"`
ClientSettingDomains map[dns.Domain]struct{} `sconf:"-" json:"-"`
}

type ACME struct {
Expand All @@ -138,7 +140,7 @@ type Listener struct {
IPs []string `sconf-doc:"Use 0.0.0.0 to listen on all IPv4 and/or :: to listen on all IPv6 addresses, but it is better to explicitly specify the IPs you want to use for email, as mox will make sure outgoing connections will only be made from one of those IPs. If both outgoing IPv4 and IPv6 connectivity is possible, and only one family has explicitly configured addresses, both address families are still used for outgoing connections. Use the \"direct\" transport to limit address families for outgoing connections."`
NATIPs []string `sconf:"optional" sconf-doc:"If set, the mail server is configured behind a NAT and field IPs are internal instead of the public IPs, while NATIPs lists the public IPs. Used during IP-related DNS self-checks, such as for iprev, mx, spf, autoconfig, autodiscover, and for autotls."`
IPsNATed bool `sconf:"optional" sconf-doc:"Deprecated, use NATIPs instead. If set, IPs are not the public IPs, but are NATed. Skips IP-related DNS self-checks."`
Hostname string `sconf:"optional" sconf-doc:"If empty, the config global Hostname is used."`
Hostname string `sconf:"optional" sconf-doc:"If empty, the config global Hostname is used. The internal services webadmin, webaccount, webmail and webapi only match requests to IPs, this hostname, \"localhost\". All except webadmin also match for any client settings domain."`
HostnameDomain dns.Domain `sconf:"-" json:"-"` // Set when parsing config.

TLS *TLS `sconf:"optional" sconf-doc:"For SMTP/IMAP STARTTLS, direct TLS and HTTPS connections."`
Expand Down Expand Up @@ -215,7 +217,7 @@ type Listener struct {
// WebService is an internal web interface: webmail, webaccount, webadmin, webapi.
type WebService struct {
Enabled bool
Port int `sconf:"optional" sconf-doc:"Default 80 for HTTP and 443 for HTTPS."`
Port int `sconf:"optional" sconf-doc:"Default 80 for HTTP and 443 for HTTPS. See Hostname at Listener for hostname matching behaviour."`
Path string `sconf:"optional" sconf-doc:"Path to serve requests on."`
Forwarded bool `sconf:"optional" sconf-doc:"If set, X-Forwarded-* headers are used for the remote IP address for rate limiting and for the \"secure\" status of cookies."`
}
Expand Down Expand Up @@ -521,15 +523,18 @@ type TLS struct {
HostPrivateECDSAP256Keys []crypto.Signer `sconf:"-" json:"-"`
}

// todo: we could implement matching WebHandler.Domain as IPs too

type WebHandler struct {
LogName string `sconf:"optional" sconf-doc:"Name to use in logging and metrics."`
Domain string `sconf-doc:"Both Domain and PathRegexp must match for this WebHandler to match a request. Exactly one of WebStatic, WebRedirect, WebForward must be set."`
Domain string `sconf-doc:"Both Domain and PathRegexp must match for this WebHandler to match a request. Exactly one of WebStatic, WebRedirect, WebForward, WebInternal must be set."`
PathRegexp string `sconf-doc:"Regular expression matched against request path, must always start with ^ to ensure matching from the start of the path. The matching prefix can optionally be stripped by WebForward. The regular expression does not have to end with $."`
DontRedirectPlainHTTP bool `sconf:"optional" sconf-doc:"If set, plain HTTP requests are not automatically permanently redirected (308) to HTTPS. If you don't have a HTTPS webserver configured, set this to true."`
Compress bool `sconf:"optional" sconf-doc:"Transparently compress responses (currently with gzip) if the client supports it, the status is 200 OK, no Content-Encoding is set on the response yet and the Content-Type of the response hints that the data is compressible (text/..., specific application/... and .../...+json and .../...+xml). For static files only, a cache with compressed files is kept."`
WebStatic *WebStatic `sconf:"optional" sconf-doc:"Serve static files."`
WebRedirect *WebRedirect `sconf:"optional" sconf-doc:"Redirect requests to configured URL."`
WebForward *WebForward `sconf:"optional" sconf-doc:"Forward requests to another webserver, i.e. reverse proxy."`
WebInternal *WebInternal `sconf:"optional" sconf-doc:"Pass request to internal service, like webmail, webapi, etc."`

Name string `sconf:"-"` // Either LogName, or numeric index if LogName was empty. Used instead of LogName in logging/metrics.
DNSDomain dns.Domain `sconf:"-"`
Expand All @@ -545,14 +550,15 @@ func (wh WebHandler) Equal(o WebHandler) bool {
x.WebStatic = nil
x.WebRedirect = nil
x.WebForward = nil
x.WebInternal = nil
return x
}
cwh := clean(wh)
co := clean(o)
if cwh != co {
return false
}
if (wh.WebStatic == nil) != (o.WebStatic == nil) || (wh.WebRedirect == nil) != (o.WebRedirect == nil) || (wh.WebForward == nil) != (o.WebForward == nil) {
if (wh.WebStatic == nil) != (o.WebStatic == nil) || (wh.WebRedirect == nil) != (o.WebRedirect == nil) || (wh.WebForward == nil) != (o.WebForward == nil) || (wh.WebInternal == nil) != (o.WebInternal == nil) {
return false
}
if wh.WebStatic != nil {
Expand All @@ -564,6 +570,9 @@ func (wh WebHandler) Equal(o WebHandler) bool {
if wh.WebForward != nil {
return wh.WebForward.equal(*o.WebForward)
}
if wh.WebInternal != nil {
return wh.WebInternal.equal(*o.WebInternal)
}
return true
}

Expand Down Expand Up @@ -606,3 +615,16 @@ func (wf WebForward) equal(o WebForward) bool {
o.TargetURL = nil
return reflect.DeepEqual(wf, o)
}

type WebInternal struct {
BasePath string `sconf-doc:"Path to use as root of internal service, e.g. /webmail/."`
Service string `sconf-doc:"Name of the service, values: admin, account, webmail, webapi."`

Handler http.Handler `sconf:"-" json:"-"`
}

func (wi WebInternal) equal(o WebInternal) bool {
wi.Handler = nil
o.Handler = nil
return reflect.DeepEqual(wi, o)
}
55 changes: 39 additions & 16 deletions config/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,10 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
# NATed. Skips IP-related DNS self-checks. (optional)
IPsNATed: false
# If empty, the config global Hostname is used. (optional)
# If empty, the config global Hostname is used. The internal services webadmin,
# webaccount, webmail and webapi only match requests to IPs, this hostname,
# "localhost". All except webadmin also match for any client settings domain.
# (optional)
Hostname:
# For SMTP/IMAP STARTTLS, direct TLS and HTTPS connections. (optional)
Expand Down Expand Up @@ -303,7 +306,8 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
AccountHTTP:
Enabled: false
# Default 80 for HTTP and 443 for HTTPS. (optional)
# Default 80 for HTTP and 443 for HTTPS. See Hostname at Listener for hostname
# matching behaviour. (optional)
Port: 0
# Path to serve requests on. (optional)
Expand All @@ -318,7 +322,8 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
AccountHTTPS:
Enabled: false
# Default 80 for HTTP and 443 for HTTPS. (optional)
# Default 80 for HTTP and 443 for HTTPS. See Hostname at Listener for hostname
# matching behaviour. (optional)
Port: 0
# Path to serve requests on. (optional)
Expand All @@ -336,7 +341,8 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
AdminHTTP:
Enabled: false
# Default 80 for HTTP and 443 for HTTPS. (optional)
# Default 80 for HTTP and 443 for HTTPS. See Hostname at Listener for hostname
# matching behaviour. (optional)
Port: 0
# Path to serve requests on. (optional)
Expand All @@ -351,7 +357,8 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
AdminHTTPS:
Enabled: false
# Default 80 for HTTP and 443 for HTTPS. (optional)
# Default 80 for HTTP and 443 for HTTPS. See Hostname at Listener for hostname
# matching behaviour. (optional)
Port: 0
# Path to serve requests on. (optional)
Expand All @@ -365,7 +372,8 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
WebmailHTTP:
Enabled: false
# Default 80 for HTTP and 443 for HTTPS. (optional)
# Default 80 for HTTP and 443 for HTTPS. See Hostname at Listener for hostname
# matching behaviour. (optional)
Port: 0
# Path to serve requests on. (optional)
Expand All @@ -380,7 +388,8 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
WebmailHTTPS:
Enabled: false
# Default 80 for HTTP and 443 for HTTPS. (optional)
# Default 80 for HTTP and 443 for HTTPS. See Hostname at Listener for hostname
# matching behaviour. (optional)
Port: 0
# Path to serve requests on. (optional)
Expand All @@ -394,7 +403,8 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
WebAPIHTTP:
Enabled: false
# Default 80 for HTTP and 443 for HTTPS. (optional)
# Default 80 for HTTP and 443 for HTTPS. See Hostname at Listener for hostname
# matching behaviour. (optional)
Port: 0
# Path to serve requests on. (optional)
Expand All @@ -409,7 +419,8 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
WebAPIHTTPS:
Enabled: false
# Default 80 for HTTP and 443 for HTTPS. (optional)
# Default 80 for HTTP and 443 for HTTPS. See Hostname at Listener for hostname
# matching behaviour. (optional)
Port: 0
# Path to serve requests on. (optional)
Expand Down Expand Up @@ -1225,20 +1236,23 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
WebDomainRedirects:
x:
# Handle webserver requests by serving static files, redirecting or
# reverse-proxying HTTP(s). The first matching WebHandler will handle the request.
# Built-in handlers, e.g. for account, admin, autoconfig and mta-sts always run
# first. If no handler matches, the response status code is file not found (404).
# If functionality you need is missng, simply forward the requests to an
# application that can provide the needed functionality. (optional)
# Handle webserver requests by serving static files, redirecting, reverse-proxying
# HTTP(s) or passing the request to an internal service. The first matching
# WebHandler will handle the request. Built-in system handlers, e.g. for ACME
# validation, autoconfig and mta-sts always run first. Built-in handlers for
# admin, account, webmail and webapi are evaluated after all handlers, including
# webhandlers (allowing for overrides of internal services for some domains). If
# no handler matches, the response status code is file not found (404). If
# webserver features are missing, forward the requests to an application that
# provides the needed functionality itself. (optional)
WebHandlers:
-
# Name to use in logging and metrics. (optional)
LogName:
# Both Domain and PathRegexp must match for this WebHandler to match a request.
# Exactly one of WebStatic, WebRedirect, WebForward must be set.
# Exactly one of WebStatic, WebRedirect, WebForward, WebInternal must be set.
Domain:
# Regular expression matched against request path, must always start with ^ to
Expand Down Expand Up @@ -1345,6 +1359,15 @@ See https://pkg.go.dev/github.com/mjl-/sconf for details.
ResponseHeaders:
x:
# Pass request to internal service, like webmail, webapi, etc. (optional)
WebInternal:
# Path to use as root of internal service, e.g. /webmail/.
BasePath:
# Name of the service, values: admin, account, webmail, webapi.
Service:
# Routes for delivering outgoing messages through the queue. Each delivery attempt
# evaluates account routes, domain routes and finally these global routes. The
# transport of the first matching route is used in the delivery attempt. If no
Expand Down
Loading

0 comments on commit 614576e

Please sign in to comment.