diff --git a/config/config.go b/config/config.go index 521c3f3..145d6fc 100644 --- a/config/config.go +++ b/config/config.go @@ -73,7 +73,7 @@ type Dynamic struct { Domains map[string]Domain `sconf-doc:"Domains for which email is accepted. For internationalized domains, use their IDNA names in UTF-8."` Accounts map[string]Account `sconf-doc:"Accounts to which email can be delivered. An account can accept email for multiple domains, for multiple localparts, and deliver to multiple mailboxes."` 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 for 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 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."` WebDNSDomainRedirects map[dns.Domain]dns.Domain `sconf:"-"` } @@ -366,7 +366,7 @@ type WebStatic struct { } type WebRedirect struct { - BaseURL string `sconf:"optional" sconf-doc:"Base URL to redirect to. The path must be empty and will be replaced, either by the request URL path, or by OrigPathRegexp/ReplacePath. Scheme, host, port and fragment stay intact, and query strings are combined. If empty, the response redirects to a different path through OrigPathRegexp and ReplacePath, which must then be set. Use a URL without scheme to redirect without changing the protocol, e.g. //newdomain/."` + BaseURL string `sconf:"optional" sconf-doc:"Base URL to redirect to. The path must be empty and will be replaced, either by the request URL path, or by OrigPathRegexp/ReplacePath. Scheme, host, port and fragment stay intact, and query strings are combined. If empty, the response redirects to a different path through OrigPathRegexp and ReplacePath, which must then be set. Use a URL without scheme to redirect without changing the protocol, e.g. //newdomain/. If a redirect would send a request to a URL with the same scheme, host and path, the WebRedirect does not match so a next WebHandler can be tried. This can be used to redirect all plain http traffic to https."` OrigPathRegexp string `sconf:"optional" sconf-doc:"Regular expression for matching path. If set and path does not match, a 404 is returned. The HTTP path used for matching always starts with a slash."` ReplacePath string `sconf:"optional" sconf-doc:"Replacement path for destination URL based on OrigPathRegexp. Implemented with Go's Regexp.ReplaceAllString: $1 is replaced with the text of the first submatch, etc. If both OrigPathRegexp and ReplacePath are empty, BaseURL must be set and all paths are redirected unaltered."` StatusCode int `sconf:"optional" sconf-doc:"Status code to use in redirect, e.g. 307. By default, a permanent redirect (308) is returned."` diff --git a/config/doc.go b/config/doc.go index 408e1e8..7a7659b 100644 --- a/config/doc.go +++ b/config/doc.go @@ -570,10 +570,10 @@ describe-static" and "mox config describe-domains": # Handle webserver requests by serving static files, redirecting or # reverse-proxying HTTP(s). The first matching WebHandler will handle the request. - # Built-in handlers for 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) + # 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) WebHandlers: - @@ -637,7 +637,10 @@ describe-static" and "mox config describe-domains": # fragment stay intact, and query strings are combined. If empty, the response # redirects to a different path through OrigPathRegexp and ReplacePath, which must # then be set. Use a URL without scheme to redirect without changing the protocol, - # e.g. //newdomain/. (optional) + # e.g. //newdomain/. If a redirect would send a request to a URL with the same + # scheme, host and path, the WebRedirect does not match so a next WebHandler can + # be tried. This can be used to redirect all plain http traffic to https. + # (optional) BaseURL: # Regular expression for matching path. If set and path does not match, a 404 is @@ -689,6 +692,20 @@ examples with "mox example", and print a specific example with "mox example # Each request is matched against these handlers until one matches and serves it. WebHandlers: + - + # Redirect all plain http requests to https, leaving path, query strings, etc + # intact. When the request is already to https, the destination URL would have the + # same scheme, host and path, causing this redirect handler to not match the + # request (and not cause a redirect loop) and the webserver to serve the request + # with a later handler. + LogName: redirhttps + Domain: www.mox.example + PathRegexp: ^/ + # Could leave DontRedirectPlainHTTP at false if it wasn't for this being an + # example for doing this redirect. + DontRedirectPlainHTTP: true + WebRedirect: + BaseURL: https://www.mox.example - # The name of the handler, used in logging and metrics. LogName: staticmjl diff --git a/http/admin.html b/http/admin.html index 1dc5b14..656b152 100644 --- a/http/admin.html +++ b/http/admin.html @@ -1754,7 +1754,7 @@ const webserver = async () => { dom.td('Type'), dom.td( 'BaseURL', - attr({title: 'Base URL to redirect to. The path must be empty and will be replaced, either by the request URL path, or by OrigPathRegexp/ReplacePath. Scheme, host, port and fragment stay intact, and query strings are combined. If empty, the response redirects to a different path through OrigPathRegexp and ReplacePath, which must then be set. Use a URL without scheme to redirect without changing the protocol, e.g. //newdomain/.'}), + attr({title: 'Base URL to redirect to. The path must be empty and will be replaced, either by the request URL path, or by OrigPathRegexp/ReplacePath. Scheme, host, port and fragment stay intact, and query strings are combined. If empty, the response redirects to a different path through OrigPathRegexp and ReplacePath, which must then be set. Use a URL without scheme to redirect without changing the protocol, e.g. //newdomain/. If a redirect would send a request to a URL with the same scheme, host and path, the WebRedirect does not match so a next WebHandler can be tried. This can be used to redirect all plain http traffic to https.'}), ), dom.td( 'OrigPathRegexp', diff --git a/http/webserver.go b/http/webserver.go index 87f57ed..7d4e0e6 100644 --- a/http/webserver.go +++ b/http/webserver.go @@ -341,6 +341,19 @@ func HandleRedirect(h *config.WebRedirect, w http.ResponseWriter, r *http.Reques if h.StatusCode != 0 { code = h.StatusCode } + + // If we would be redirecting to the same scheme,host,path, we would get here again + // causing a redirect loop. Instead, this causes this redirect to not match, + // allowing to try the next WebHandler. This can be used to redirect all plain http + // requests to https. + reqscheme := "http" + if r.TLS != nil { + reqscheme = "https" + } + if reqscheme == u.Scheme && r.Host == u.Host && r.URL.Path == u.Path { + return false + } + http.Redirect(w, r, u.String(), code) return true } diff --git a/http/webserver_test.go b/http/webserver_test.go index 6a1747f..291fd57 100644 --- a/http/webserver_test.go +++ b/http/webserver_test.go @@ -50,6 +50,10 @@ func TestWebserver(t *testing.T) { test("GET", "http://redir.mox.example", nil, http.StatusPermanentRedirect, "", map[string]string{"Location": "https://mox.example/"}) + // http to https redirect, and stay on https afterwards without redirect loop. + test("GET", "http://schemeredir.example", nil, http.StatusPermanentRedirect, "", map[string]string{"Location": "https://schemeredir.example/"}) + test("GET", "https://schemeredir.example", nil, http.StatusNotFound, "", nil) + test("GET", "http://mox.example/static/", nil, http.StatusOK, "", map[string]string{"X-Test": "mox"}) // index.html test("GET", "http://mox.example/static/dir/", nil, http.StatusOK, "", map[string]string{"X-Test": "mox"}) // listing test("GET", "http://mox.example/static/dir", nil, http.StatusTemporaryRedirect, "", map[string]string{"Location": "/static/dir/"}) // redirect to dir diff --git a/main.go b/main.go index 7c60b65..b7a658c 100644 --- a/main.go +++ b/main.go @@ -776,6 +776,20 @@ WebDomainRedirects: # Each request is matched against these handlers until one matches and serves it. WebHandlers: + - + # Redirect all plain http requests to https, leaving path, query strings, etc + # intact. When the request is already to https, the destination URL would have the + # same scheme, host and path, causing this redirect handler to not match the + # request (and not cause a redirect loop) and the webserver to serve the request + # with a later handler. + LogName: redirhttps + Domain: www.mox.example + PathRegexp: ^/ + # Could leave DontRedirectPlainHTTP at false if it wasn't for this being an + # example for doing this redirect. + DontRedirectPlainHTTP: true + WebRedirect: + BaseURL: https://www.mox.example - # The name of the handler, used in logging and metrics. LogName: staticmjl diff --git a/testdata/webserver/domains.conf b/testdata/webserver/domains.conf index 88f43fd..e62bb49 100644 --- a/testdata/webserver/domains.conf +++ b/testdata/webserver/domains.conf @@ -9,6 +9,13 @@ Accounts: WebDomainRedirects: redir.mox.example: mox.example WebHandlers: + - + LogName: redirhttps + Domain: schemeredir.example + PathRegexp: ^/ + DontRedirectPlainHTTP: true + WebRedirect: + BaseURL: https://schemeredir.example - LogName: static Domain: mox.example