Browse Source

caddyhttp: New algorithm for auto HTTP->HTTPS redirects (fix #3127) (#3128)

It's still not perfect but I think it should be more correct for
slightly more complex configs. Might still fall apart for complex
configs that use on-demand TLS or at a large scale (workarounds are
to just implement your own redirects, very easy to do anyway).
master
Matt Holt 5 years ago
committed by GitHub
parent
commit
2762f8f058
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 170
      modules/caddyhttp/autohttps.go

170
modules/caddyhttp/autohttps.go

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
@ -62,12 +61,14 @@ func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
// even servers to the app, which still need to be set up with the // even servers to the app, which still need to be set up with the
// rest of them during provisioning. // rest of them during provisioning.
func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) error { func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) error {
// this map will store associations of HTTP listener // this map acts as a set to store the domain names
// addresses to the routes that do HTTP->HTTPS redirects // for which we will manage certificates automatically
lnAddrRedirRoutes := make(map[string]Route)
uniqueDomainsForCerts := make(map[string]struct{}) uniqueDomainsForCerts := make(map[string]struct{})
// this maps domain names for automatic HTTP->HTTPS
// redirects to their destination server address
redirDomains := make(map[string]caddy.ParsedAddress)
for srvName, srv := range app.Servers { for srvName, srv := range app.Servers {
// as a prerequisite, provision route matchers; this is // as a prerequisite, provision route matchers; this is
// required for all routes on all servers, and must be // required for all routes on all servers, and must be
@ -180,39 +181,80 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// create HTTP->HTTPS redirects // create HTTP->HTTPS redirects
for _, addr := range srv.Listen { for _, addr := range srv.Listen {
netw, host, port, err := caddy.SplitNetworkAddress(addr) // figure out the address we will redirect to...
addr, err := caddy.ParseNetworkAddress(addr)
if err != nil { if err != nil {
return fmt.Errorf("%s: invalid listener address: %v", srvName, addr) return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
} }
if parts := strings.SplitN(port, "-", 2); len(parts) == 2 { // ...and associate it with each domain in this server
port = parts[0] for d := range serverDomainSet {
// if this domain is used on more than one HTTPS-enabled
// port, we'll have to choose one, so prefer the HTTPS port
if _, ok := redirDomains[d]; !ok ||
addr.StartPort == uint(app.httpsPort()) {
redirDomains[d] = addr
}
}
}
} }
redirTo := "https://{http.request.host}"
if port != strconv.Itoa(app.httpsPort()) { // we now have a list of all the unique names for which we need certs;
redirTo += ":" + port // turn the set into a slice so that phase 2 can use it
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
for d := range uniqueDomainsForCerts {
app.allCertDomains = append(app.allCertDomains, d)
} }
redirTo += "{http.request.uri}"
// build the plaintext HTTP variant of this address // ensure there is an automation policy to handle these certs
httpRedirLnAddr := caddy.JoinNetworkAddress(netw, host, strconv.Itoa(app.httpPort())) err := app.createAutomationPolicy(ctx)
if err != nil {
return err
}
// we're done if there are no HTTP->HTTPS redirects to add
if len(redirDomains) == 0 {
return nil
}
// we need to reduce the mapping, i.e. group domains by address
// since new routes are appended to servers by their address
domainsByAddr := make(map[string][]string)
for domain, addr := range redirDomains {
addrStr := addr.String()
domainsByAddr[addrStr] = append(domainsByAddr[addrStr], domain)
}
// these keep track of the redirect server address(es)
// and the routes for those servers which actually
// respond with the redirects
redirServerAddrs := make(map[string]struct{})
var redirRoutes RouteList
redirServers := make(map[string][]Route)
for addrStr, domains := range domainsByAddr {
// build the matcher set for this redirect route // build the matcher set for this redirect route
// (note that we happen to bypass Provision and // (note that we happen to bypass Provision and
// Validate steps for these matcher modules) // Validate steps for these matcher modules)
matcherSet := MatcherSet{MatchProtocol("http")} matcherSet := MatcherSet{
if len(srv.AutoHTTPS.Skip) > 0 { MatchProtocol("http"),
matcherSet = append(matcherSet, MatchNegate{ MatchHost(domains),
Matchers: MatcherSet{MatchHost(srv.AutoHTTPS.Skip)},
})
} }
// create the route that does the redirect and associate // build the address to which to redirect
// it with the listener address it will be served from addr, err := caddy.ParseNetworkAddress(addrStr)
// (note that we happen to bypass any Provision or Validate if err != nil {
// steps on the handler modules created here) return err
lnAddrRedirRoutes[httpRedirLnAddr] = Route{ }
redirTo := "https://{http.request.host}"
if addr.StartPort != DefaultHTTPSPort {
redirTo += ":" + strconv.Itoa(int(addr.StartPort))
}
redirTo += "{http.request.uri}"
// build the route
redirRoute := Route{
MatcherSets: []MatcherSet{matcherSet}, MatcherSets: []MatcherSet{matcherSet},
Handlers: []MiddlewareHandler{ Handlers: []MiddlewareHandler{
StaticResponse{ StaticResponse{
@ -225,65 +267,87 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
}, },
}, },
} }
}
}
// we now have a list of all the unique names for which we need certs; // use the network/host information from the address,
// turn the set into a slice so that phase 2 can use it // but change the port to the HTTP port then rebuild
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) redirAddr := addr
for d := range uniqueDomainsForCerts { redirAddr.StartPort = uint(app.httpPort())
app.allCertDomains = append(app.allCertDomains, d) redirAddr.EndPort = redirAddr.StartPort
} redirAddrStr := redirAddr.String()
// ensure there is an automation policy to handle these certs redirServers[redirAddrStr] = append(redirServers[redirAddrStr], redirRoute)
err := app.createAutomationPolicy(ctx)
if err != nil {
return err
} }
// if there are HTTP->HTTPS redirects to add, do so now // on-demand TLS means that hostnames may be used which are not
if len(lnAddrRedirRoutes) == 0 { // explicitly defined in the config, and we still need to redirect
return nil // those; so we can append a single catch-all route (notice there
// is no Host matcher) after the other redirect routes which will
// allow us to handle unexpected/new hostnames... however, it's
// not entirely clear what the redirect destination should be,
// so I'm going to just hard-code the app's HTTPS port and call
// it good for now...
appendCatchAll := func(routes []Route) []Route {
redirTo := "https://{http.request.host}"
if app.httpsPort() != DefaultHTTPSPort {
redirTo += ":" + strconv.Itoa(app.httpsPort())
}
redirTo += "{http.request.uri}"
routes = append(routes, Route{
MatcherSets: []MatcherSet{MatcherSet{MatchProtocol("http")}},
Handlers: []MiddlewareHandler{
StaticResponse{
StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
Headers: http.Header{
"Location": []string{redirTo},
"Connection": []string{"close"},
},
Close: true,
},
},
})
return routes
} }
var redirServerAddrs []string redirServersLoop:
var redirRoutes RouteList for redirServerAddr, routes := range redirServers {
// for each redirect listener, see if there's already a // for each redirect listener, see if there's already a
// server configured to listen on that exact address; if so, // server configured to listen on that exact address; if so,
// simply add the redirect route to the end of its route // simply add the redirect route to the end of its route
// list; otherwise, we'll create a new server for all the // list; otherwise, we'll create a new server for all the
// listener addresses that are unused and serve the // listener addresses that are unused and serve the
// remaining redirects from it // remaining redirects from it
redirRoutesLoop:
for addr, redirRoute := range lnAddrRedirRoutes {
for srvName, srv := range app.Servers { for srvName, srv := range app.Servers {
if srv.hasListenerAddress(addr) { if srv.hasListenerAddress(redirServerAddr) {
// user has configured a server for the same address // user has configured a server for the same address
// that the redirect runs from; simply append our // that the redirect runs from; simply append our
// redirect route to the existing routes, with a // redirect route to the existing routes, with a
// caveat that their config might override ours // caveat that their config might override ours
app.logger.Warn("server is listening on same interface as redirects, so automatic HTTP->HTTPS redirects might be overridden by your own configuration", app.logger.Warn("user server is listening on same interface as automatic HTTP->HTTPS redirects; user-configured routes might override these redirects",
zap.String("server_name", srvName), zap.String("server_name", srvName),
zap.String("interface", addr), zap.String("interface", redirServerAddr),
) )
srv.Routes = append(srv.Routes, redirRoute) srv.Routes = append(srv.Routes, appendCatchAll(routes)...)
continue redirRoutesLoop continue redirServersLoop
} }
} }
// no server with this listener address exists; // no server with this listener address exists;
// save this address and route for custom server // save this address and route for custom server
redirServerAddrs = append(redirServerAddrs, addr) redirServerAddrs[redirServerAddr] = struct{}{}
redirRoutes = append(redirRoutes, redirRoute) redirRoutes = append(redirRoutes, routes...)
} }
// if there are routes remaining which do not belong // if there are routes remaining which do not belong
// in any existing server, make our own to serve the // in any existing server, make our own to serve the
// rest of the redirects // rest of the redirects
if len(redirServerAddrs) > 0 { if len(redirServerAddrs) > 0 {
redirServerAddrsList := make([]string, 0, len(redirServerAddrs))
for a := range redirServerAddrs {
redirServerAddrsList = append(redirServerAddrsList, a)
}
app.Servers["remaining_auto_https_redirects"] = &Server{ app.Servers["remaining_auto_https_redirects"] = &Server{
Listen: redirServerAddrs, Listen: redirServerAddrsList,
Routes: redirRoutes, Routes: appendCatchAll(redirRoutes),
} }
} }

Loading…
Cancel
Save