diff --git a/caddy/letsencrypt/handler.go b/caddy/letsencrypt/handler.go index c97d47dfb..4fdf4f625 100644 --- a/caddy/letsencrypt/handler.go +++ b/caddy/letsencrypt/handler.go @@ -2,62 +2,73 @@ package letsencrypt import ( "crypto/tls" + "net" "net/http" "net/http/httputil" "net/url" - "sync" + "strings" "sync/atomic" "github.com/mholt/caddy/middleware" ) -// Handler is a Caddy middleware that can proxy ACME requests -// to the real ACME endpoint. This is necessary to renew certificates -// while the server is running. Obviously, a site served on port -// 443 (HTTPS) binds to that port, so another listener created by -// our acme client can't bind successfully and solve the challenge. -// Thus, we chain this handler in so that it can, when activated, -// proxy ACME requests to an ACME client listening on an alternate -// port. +const challengeBasePath = "/.well-known/acme-challenge" + +// Handler is a Caddy middleware that can proxy ACME challenge +// requests to the real ACME client endpoint. This is necessary +// to renew certificates while the server is running. type Handler struct { - sync.Mutex // protects the ChallengePath property Next middleware.Handler - ChallengeActive int32 // use sync/atomic for speed to set/get this flag - ChallengePath string // the exact request path to match before proxying + ChallengeActive int32 // TODO: use sync/atomic to set/get this flag safely and efficiently } // ServeHTTP is basically a no-op unless an ACME challenge is active on this host // and the request path matches the expected path exactly. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { // Only if challenge is active - if atomic.LoadInt32(&h.ChallengeActive) == 1 { - h.Lock() - path := h.ChallengePath - h.Unlock() + // TODO: this won't work until the global challenge hook in the acme package is ready + //if atomic.LoadInt32(&h.ChallengeActive) == 1 { - // Request path must be correct; if so, proxy to ACME client - if r.URL.Path == path { - upstream, err := url.Parse("https://" + r.Host + ":" + alternatePort) - if err != nil { - return http.StatusInternalServerError, err - } - proxy := httputil.NewSingleHostReverseProxy(upstream) - proxy.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client uses self-signed cert - } - proxy.ServeHTTP(w, r) - return 0, nil + // Proxy challenge requests to ACME client + if strings.HasPrefix(r.URL.Path, challengeBasePath) { + scheme := "http" + if r.TLS != nil { + scheme = "https" } + + hostname, _, err := net.SplitHostPort(r.URL.Host) + if err != nil { + hostname = r.URL.Host + } + + upstream, err := url.Parse(scheme + "://" + hostname + ":" + alternatePort) + if err != nil { + return http.StatusInternalServerError, err + } + + proxy := httputil.NewSingleHostReverseProxy(upstream) + proxy.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client uses self-signed cert + } + proxy.ServeHTTP(w, r) + + return 0, nil } + //} + return h.Next.ServeHTTP(w, r) } +// TODO: SimpleHTTP deprecation imminent!! meaning these +// challenge handlers will go away and be replaced with +// something else. + // ChallengeOn enables h to proxy ACME requests. func (h *Handler) ChallengeOn(challengePath string) { - h.Lock() - h.ChallengePath = challengePath - h.Unlock() + // h.Lock() + // h.ChallengePath = challengePath + // h.Unlock() atomic.StoreInt32(&h.ChallengeActive, 1) } diff --git a/caddy/letsencrypt/letsencrypt.go b/caddy/letsencrypt/letsencrypt.go index 0ac36f020..27c084b57 100644 --- a/caddy/letsencrypt/letsencrypt.go +++ b/caddy/letsencrypt/letsencrypt.go @@ -344,24 +344,34 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config { cfg.Port = "https" } - // Chain in ACME middleware proxy if we use up the SSL port - if cfg.Port == "https" || cfg.Port == "443" { - handler := new(Handler) - mid := func(next middleware.Handler) middleware.Handler { - handler.Next = next - return handler - } - cfg.Middleware["/"] = append(cfg.Middleware["/"], mid) - acmeHandlers[cfg.Host] = handler - } - // Set up http->https redirect as long as there isn't already a http counterpart - // in the configs and this isn't, for some reason, already on port 80 + // in the configs and this isn't, for some reason, already on port 80. + // Also, the port 80 variant of this config is necessary for proxying challenge requests. if !otherHostHasScheme(allConfigs, cfgIndex, "http") && cfg.Port != "80" && cfg.Port != "http" { // (would not be http port with current program flow, but just in case) allConfigs = append(allConfigs, redirPlaintextHost(*cfg)) } + // To support renewals, we need handlers at ports 80 and 443, + // depending on the challenge type that is used to complete renewal. + // Every proxy for this host can share the handler. + handler := new(Handler) + mid := func(next middleware.Handler) middleware.Handler { + handler.Next = next + return handler + } + acmeHandlers[cfg.Host] = handler + + // Handler needs to go in 80 and 443 + for i, c := range allConfigs { + if c.Address() == cfg.Host+":80" || + c.Address() == cfg.Host+":443" || + c.Address() == cfg.Host+":http" || + c.Address() == cfg.Host+":https" { + allConfigs[i].Middleware["/"] = append(allConfigs[i].Middleware["/"], mid) + } + } + return allConfigs } diff --git a/caddy/letsencrypt/maintain.go b/caddy/letsencrypt/maintain.go index 8f1416575..6eaa8220a 100644 --- a/caddy/letsencrypt/maintain.go +++ b/caddy/letsencrypt/maintain.go @@ -144,9 +144,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro acme.OnSimpleHTTPStart = acmeHandlers[cfg.Host].ChallengeOn acme.OnSimpleHTTPEnd = acmeHandlers[cfg.Host].ChallengeOff - // Renew certificate. - // TODO: revokeOld should be an option in the caddyfile - // TODO: bundle should be an option in the caddyfile as well :) + // Renew certificate Renew: newCertMeta, err := client.RenewCertificate(certMeta, true, true) if err != nil {