letsencrypt: Support for http-01, awkwardly straddling that and SimpleHTTP for now

This commit is contained in:
Matthew Holt 2015-11-17 18:11:19 -07:00
parent 659df6967e
commit 580b50ea20
3 changed files with 65 additions and 46 deletions

View File

@ -2,62 +2,73 @@ package letsencrypt
import ( import (
"crypto/tls" "crypto/tls"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"sync" "strings"
"sync/atomic" "sync/atomic"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
) )
// Handler is a Caddy middleware that can proxy ACME requests const challengeBasePath = "/.well-known/acme-challenge"
// to the real ACME endpoint. This is necessary to renew certificates
// while the server is running. Obviously, a site served on port // Handler is a Caddy middleware that can proxy ACME challenge
// 443 (HTTPS) binds to that port, so another listener created by // requests to the real ACME client endpoint. This is necessary
// our acme client can't bind successfully and solve the challenge. // to renew certificates while the server is running.
// Thus, we chain this handler in so that it can, when activated,
// proxy ACME requests to an ACME client listening on an alternate
// port.
type Handler struct { type Handler struct {
sync.Mutex // protects the ChallengePath property
Next middleware.Handler Next middleware.Handler
ChallengeActive int32 // use sync/atomic for speed to set/get this flag ChallengeActive int32 // TODO: use sync/atomic to set/get this flag safely and efficiently
ChallengePath string // the exact request path to match before proxying
} }
// ServeHTTP is basically a no-op unless an ACME challenge is active on this host // ServeHTTP is basically a no-op unless an ACME challenge is active on this host
// and the request path matches the expected path exactly. // and the request path matches the expected path exactly.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// Only if challenge is active // Only if challenge is active
if atomic.LoadInt32(&h.ChallengeActive) == 1 { // TODO: this won't work until the global challenge hook in the acme package is ready
h.Lock() //if atomic.LoadInt32(&h.ChallengeActive) == 1 {
path := h.ChallengePath
h.Unlock()
// Request path must be correct; if so, proxy to ACME client // Proxy challenge requests to ACME client
if r.URL.Path == path { if strings.HasPrefix(r.URL.Path, challengeBasePath) {
upstream, err := url.Parse("https://" + r.Host + ":" + alternatePort) scheme := "http"
if err != nil { if r.TLS != nil {
return http.StatusInternalServerError, err scheme = "https"
}
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
} }
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) 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. // ChallengeOn enables h to proxy ACME requests.
func (h *Handler) ChallengeOn(challengePath string) { func (h *Handler) ChallengeOn(challengePath string) {
h.Lock() // h.Lock()
h.ChallengePath = challengePath // h.ChallengePath = challengePath
h.Unlock() // h.Unlock()
atomic.StoreInt32(&h.ChallengeActive, 1) atomic.StoreInt32(&h.ChallengeActive, 1)
} }

View File

@ -344,24 +344,34 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config {
cfg.Port = "https" 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 // 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") && if !otherHostHasScheme(allConfigs, cfgIndex, "http") &&
cfg.Port != "80" && cfg.Port != "http" { // (would not be http port with current program flow, but just in case) cfg.Port != "80" && cfg.Port != "http" { // (would not be http port with current program flow, but just in case)
allConfigs = append(allConfigs, redirPlaintextHost(*cfg)) 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 return allConfigs
} }

View File

@ -144,9 +144,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
acme.OnSimpleHTTPStart = acmeHandlers[cfg.Host].ChallengeOn acme.OnSimpleHTTPStart = acmeHandlers[cfg.Host].ChallengeOn
acme.OnSimpleHTTPEnd = acmeHandlers[cfg.Host].ChallengeOff acme.OnSimpleHTTPEnd = acmeHandlers[cfg.Host].ChallengeOff
// Renew certificate. // Renew certificate
// TODO: revokeOld should be an option in the caddyfile
// TODO: bundle should be an option in the caddyfile as well :)
Renew: Renew:
newCertMeta, err := client.RenewCertificate(certMeta, true, true) newCertMeta, err := client.RenewCertificate(certMeta, true, true)
if err != nil { if err != nil {