@marcel91200 / caddy Mirror · read-only / commits / 2762f8f0
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

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).

Matt Holt committed Mar 9, 2020 at 15:18 UTC 2762f8f058803e3bb1c55636df78b815b1d70439
1 file changed +129 -65
modules/caddyhttp/autohttps.go
+129 -65
@@ -4,7 +4,6 @@ import (
4 "fmt"
5 "net/http"
6 "strconv"
7 - "strings"
7
8 "github.com/caddyserver/caddy/v2"
9 "github.com/caddyserver/caddy/v2/modules/caddytls"
@@ -62,12 +61,14 @@ func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
61 // even servers to the app, which still need to be set up with the
62 // rest of them during provisioning.
63 func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) error {
65 - // this map will store associations of HTTP listener
66 - // addresses to the routes that do HTTP->HTTPS redirects
67 - lnAddrRedirRoutes := make(map[string]Route)
68 -
64 + // this map acts as a set to store the domain names
65 + // for which we will manage certificates automatically
66 uniqueDomainsForCerts := make(map[string]struct{})
67
68 + // this maps domain names for automatic HTTP->HTTPS
69 + // redirects to their destination server address
70 + redirDomains := make(map[string]caddy.ParsedAddress)
71 +
72 for srvName, srv := range app.Servers {
73 // as a prerequisite, provision route matchers; this is
74 // required for all routes on all servers, and must be
@@ -180,50 +181,20 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
181
182 // create HTTP->HTTPS redirects
183 for _, addr := range srv.Listen {
183 - netw, host, port, err := caddy.SplitNetworkAddress(addr)
184 + // figure out the address we will redirect to...
185 + addr, err := caddy.ParseNetworkAddress(addr)
186 if err != nil {
187 return fmt.Errorf("%s: invalid listener address: %v", srvName, addr)
188 }
189
188 - if parts := strings.SplitN(port, "-", 2); len(parts) == 2 {
189 - port = parts[0]
190 - }
191 - redirTo := "https://{http.request.host}"
192 -
193 - if port != strconv.Itoa(app.httpsPort()) {
194 - redirTo += ":" + port
195 - }
196 - redirTo += "{http.request.uri}"
197 -
198 - // build the plaintext HTTP variant of this address
199 - httpRedirLnAddr := caddy.JoinNetworkAddress(netw, host, strconv.Itoa(app.httpPort()))
200 -
201 - // build the matcher set for this redirect route
202 - // (note that we happen to bypass Provision and
203 - // Validate steps for these matcher modules)
204 - matcherSet := MatcherSet{MatchProtocol("http")}
205 - if len(srv.AutoHTTPS.Skip) > 0 {
206 - matcherSet = append(matcherSet, MatchNegate{
207 - Matchers: MatcherSet{MatchHost(srv.AutoHTTPS.Skip)},
208 - })
209 - }
210 -
211 - // create the route that does the redirect and associate
212 - // it with the listener address it will be served from
213 - // (note that we happen to bypass any Provision or Validate
214 - // steps on the handler modules created here)
215 - lnAddrRedirRoutes[httpRedirLnAddr] = Route{
216 - MatcherSets: []MatcherSet{matcherSet},
217 - Handlers: []MiddlewareHandler{
218 - StaticResponse{
219 - StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
220 - Headers: http.Header{
221 - "Location": []string{redirTo},
222 - "Connection": []string{"close"},
223 - },
224 - Close: true,
225 - },
226 - },
190 + // ...and associate it with each domain in this server
191 + for d := range serverDomainSet {
192 + // if this domain is used on more than one HTTPS-enabled
193 + // port, we'll have to choose one, so prefer the HTTPS port
194 + if _, ok := redirDomains[d]; !ok ||
195 + addr.StartPort == uint(app.httpsPort()) {
196 + redirDomains[d] = addr
197 + }
198 }
199 }
200 }
@@ -241,49 +212,142 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
212 return err
213 }
214
244 - // if there are HTTP->HTTPS redirects to add, do so now
245 - if len(lnAddrRedirRoutes) == 0 {
215 + // we're done if there are no HTTP->HTTPS redirects to add
216 + if len(redirDomains) == 0 {
217 return nil
218 }
219
249 - var redirServerAddrs []string
220 + // we need to reduce the mapping, i.e. group domains by address
221 + // since new routes are appended to servers by their address
222 + domainsByAddr := make(map[string][]string)
223 + for domain, addr := range redirDomains {
224 + addrStr := addr.String()
225 + domainsByAddr[addrStr] = append(domainsByAddr[addrStr], domain)
226 + }
227 +
228 + // these keep track of the redirect server address(es)
229 + // and the routes for those servers which actually
230 + // respond with the redirects
231 + redirServerAddrs := make(map[string]struct{})
232 var redirRoutes RouteList
233
252 - // for each redirect listener, see if there's already a
253 - // server configured to listen on that exact address; if so,
254 - // simply add the redirect route to the end of its route
255 - // list; otherwise, we'll create a new server for all the
256 - // listener addresses that are unused and serve the
257 - // remaining redirects from it
258 -redirRoutesLoop:
259 - for addr, redirRoute := range lnAddrRedirRoutes {
234 + redirServers := make(map[string][]Route)
235 +
236 + for addrStr, domains := range domainsByAddr {
237 + // build the matcher set for this redirect route
238 + // (note that we happen to bypass Provision and
239 + // Validate steps for these matcher modules)
240 + matcherSet := MatcherSet{
241 + MatchProtocol("http"),
242 + MatchHost(domains),
243 + }
244 +
245 + // build the address to which to redirect
246 + addr, err := caddy.ParseNetworkAddress(addrStr)
247 + if err != nil {
248 + return err
249 + }
250 + redirTo := "https://{http.request.host}"
251 + if addr.StartPort != DefaultHTTPSPort {
252 + redirTo += ":" + strconv.Itoa(int(addr.StartPort))
253 + }
254 + redirTo += "{http.request.uri}"
255 +
256 + // build the route
257 + redirRoute := Route{
258 + MatcherSets: []MatcherSet{matcherSet},
259 + Handlers: []MiddlewareHandler{
260 + StaticResponse{
261 + StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
262 + Headers: http.Header{
263 + "Location": []string{redirTo},
264 + "Connection": []string{"close"},
265 + },
266 + Close: true,
267 + },
268 + },
269 + }
270 +
271 + // use the network/host information from the address,
272 + // but change the port to the HTTP port then rebuild
273 + redirAddr := addr
274 + redirAddr.StartPort = uint(app.httpPort())
275 + redirAddr.EndPort = redirAddr.StartPort
276 + redirAddrStr := redirAddr.String()
277 +
278 + redirServers[redirAddrStr] = append(redirServers[redirAddrStr], redirRoute)
279 + }
280 +
281 + // on-demand TLS means that hostnames may be used which are not
282 + // explicitly defined in the config, and we still need to redirect
283 + // those; so we can append a single catch-all route (notice there
284 + // is no Host matcher) after the other redirect routes which will
285 + // allow us to handle unexpected/new hostnames... however, it's
286 + // not entirely clear what the redirect destination should be,
287 + // so I'm going to just hard-code the app's HTTPS port and call
288 + // it good for now...
289 + appendCatchAll := func(routes []Route) []Route {
290 + redirTo := "https://{http.request.host}"
291 + if app.httpsPort() != DefaultHTTPSPort {
292 + redirTo += ":" + strconv.Itoa(app.httpsPort())
293 + }
294 + redirTo += "{http.request.uri}"
295 + routes = append(routes, Route{
296 + MatcherSets: []MatcherSet{MatcherSet{MatchProtocol("http")}},
297 + Handlers: []MiddlewareHandler{
298 + StaticResponse{
299 + StatusCode: WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
300 + Headers: http.Header{
301 + "Location": []string{redirTo},
302 + "Connection": []string{"close"},
303 + },
304 + Close: true,
305 + },
306 + },
307 + })
308 + return routes
309 + }
310 +
311 +redirServersLoop:
312 + for redirServerAddr, routes := range redirServers {
313 + // for each redirect listener, see if there's already a
314 + // server configured to listen on that exact address; if so,
315 + // simply add the redirect route to the end of its route
316 + // list; otherwise, we'll create a new server for all the
317 + // listener addresses that are unused and serve the
318 + // remaining redirects from it
319 for srvName, srv := range app.Servers {
261 - if srv.hasListenerAddress(addr) {
320 + if srv.hasListenerAddress(redirServerAddr) {
321 // user has configured a server for the same address
322 // that the redirect runs from; simply append our
323 // redirect route to the existing routes, with a
324 // caveat that their config might override ours
266 - app.logger.Warn("server is listening on same interface as redirects, so automatic HTTP->HTTPS redirects might be overridden by your own configuration",
325 + app.logger.Warn("user server is listening on same interface as automatic HTTP->HTTPS redirects; user-configured routes might override these redirects",
326 zap.String("server_name", srvName),
268 - zap.String("interface", addr),
327 + zap.String("interface", redirServerAddr),
328 )
270 - srv.Routes = append(srv.Routes, redirRoute)
271 - continue redirRoutesLoop
329 + srv.Routes = append(srv.Routes, appendCatchAll(routes)...)
330 + continue redirServersLoop
331 }
332 }
333 +
334 // no server with this listener address exists;
335 // save this address and route for custom server
276 - redirServerAddrs = append(redirServerAddrs, addr)
277 - redirRoutes = append(redirRoutes, redirRoute)
336 + redirServerAddrs[redirServerAddr] = struct{}{}
337 + redirRoutes = append(redirRoutes, routes...)
338 }
339
340 // if there are routes remaining which do not belong
341 // in any existing server, make our own to serve the
342 // rest of the redirects
343 if len(redirServerAddrs) > 0 {
344 + redirServerAddrsList := make([]string, 0, len(redirServerAddrs))
345 + for a := range redirServerAddrs {
346 + redirServerAddrsList = append(redirServerAddrsList, a)
347 + }
348 app.Servers["remaining_auto_https_redirects"] = &Server{
285 - Listen: redirServerAddrs,
286 - Routes: redirRoutes,
349 + Listen: redirServerAddrsList,
350 + Routes: appendCatchAll(redirRoutes),
351 }
352 }
353