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