Commit 580b50ea authored by Matthew Holt's avatar Matthew Holt

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

parent 659df696
...@@ -2,62 +2,73 @@ package letsencrypt ...@@ -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() // 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
}
// Request path must be correct; if so, proxy to ACME client upstream, err := url.Parse(scheme + "://" + hostname + ":" + alternatePort)
if r.URL.Path == path {
upstream, err := url.Parse("https://" + r.Host + ":" + alternatePort)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
proxy := httputil.NewSingleHostReverseProxy(upstream) proxy := httputil.NewSingleHostReverseProxy(upstream)
proxy.Transport = &http.Transport{ proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client uses self-signed cert TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client uses self-signed cert
} }
proxy.ServeHTTP(w, r) proxy.ServeHTTP(w, r)
return 0, nil 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)
} }
......
...@@ -344,22 +344,32 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config { ...@@ -344,22 +344,32 @@ 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 // Set up http->https redirect as long as there isn't already a http counterpart
if cfg.Port == "https" || cfg.Port == "443" { // 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) handler := new(Handler)
mid := func(next middleware.Handler) middleware.Handler { mid := func(next middleware.Handler) middleware.Handler {
handler.Next = next handler.Next = next
return handler return handler
} }
cfg.Middleware["/"] = append(cfg.Middleware["/"], mid)
acmeHandlers[cfg.Host] = handler acmeHandlers[cfg.Host] = handler
}
// Set up http->https redirect as long as there isn't already a http counterpart // Handler needs to go in 80 and 443
// in the configs and this isn't, for some reason, already on port 80 for i, c := range allConfigs {
if !otherHostHasScheme(allConfigs, cfgIndex, "http") && if c.Address() == cfg.Host+":80" ||
cfg.Port != "80" && cfg.Port != "http" { // (would not be http port with current program flow, but just in case) c.Address() == cfg.Host+":443" ||
allConfigs = append(allConfigs, redirPlaintextHost(*cfg)) c.Address() == cfg.Host+":http" ||
c.Address() == cfg.Host+":https" {
allConfigs[i].Middleware["/"] = append(allConfigs[i].Middleware["/"], mid)
}
} }
return allConfigs return allConfigs
......
...@@ -144,9 +144,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro ...@@ -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 {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment